AngularJS Afficher typeahead sur le clic de bouton

J'utilise la directive typeahead dans AngularJS et ça marche bien. Cependant, j'aimerais avoir un bouton en dehors de l'entrée qui, lorsque vous cliquez sur, affichera le menu déroulant typeahead. Voici un extrait de ce que je suis après …

<li class="input"> <input focus-me="click" ng-model="something" typeahead="state for state in Suggestions | filter:$viewValue:stateComparator" typeahead-focus typeahead-focus-first="false" typeahead-on-select="updateTagInput(newTagName)"> <a href="" ng-click="openTypeAhead()">Open</a> </li> 

Ok, j'ai un temps absolument terrible en essayant de créer un JSFiddle ou même un Plunkr pour cela, alors je vais vous donner le code de cette directive.

Cette directive provient à l'origine de ..

Cette puissante bibliothèque Bootstrap!

… et je l'ai volé et j'ai joué avec ça. Si vous souhaitez l'utiliser, vous aurez besoin de la bibliothèque "Bootstrap" (c'est vraiment un sous-ensemble de directives angulaires) que j'ai associé. Vous pouvez créer votre propre sous-ensemble de cette bibliothèque, mais je ne suis pas tout à fait sûr de toutes les dépendances de ma directive, car j'utilise toute la bibliothèque dans mon projet. Fondamentalement, vous avez besoin d'une directive qui commence par "typeahead".

Comme vous pouvez le voir, j'ai nommé la directive wwTypeahead (que "ww" est pour WebWanderer!). C'est une directive très facile à utiliser et fonctionne comme l'original.

 <input class="form-control" type="text" spellcheck="false" ng-model="selection" ng-trim="false" placeholder="Search Here" ww-typeahead="key as key.label for key in list" typeahead-on-select="selectionMade($item, $model, $label)" typeahead-min-length="0" /> 

La partie vraiment importante à noter est l'attribut typeahead-min-length="0" qui a vraiment été le cœur de nombreuses discussions en ligne. J'ai réussi à faire fonctionner cela.

Cette directive est destinée à remplacer la directive typeahead dans la bibliothèque que j'ai liée. Votre liste typeahead sera affichée au focus de votre boîte de saisie. Non, la liste ne s'affiche pas sur le clic d'un bouton, mais j'espère qu'il y aura des étapes de bébé à partir d'ici. Si vous avez besoin d'aide pour l'implémenter, je serai ravi de vous aider.

 /* NOTE: The following directive is a modification of the Angular typeahead directive. The normal directives, unfortunately, do not allow matching on 0 length values and the user may want a returned list of all values during the lack of input. This directives was taken from ... http://angular-ui.github.io/bootstrap/ ..and modified. */ angular.module('ui.directives', []).directive('wwTypeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser', function($compile, $parse, $q, $timeout, $document, $position, typeaheadParser) { var HOT_KEYS = [9, 13, 27, 38, 40]; return { require:'ngModel', link:function(originalScope, element, attrs, modelCtrl) { //SUPPORTED ATTRIBUTES (OPTIONS) //minimal no of characters that needs to be entered before typeahead kicks-in //var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1; var testEval = originalScope.$eval(attrs.typeaheadMinLength); var minSearch = !isNaN(parseFloat(testEval)) && isFinite(testEval) || 1; //minimal wait time after last character typed before typehead kicks-in var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; //should it restrict model values to the ones selected from the popup only? var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; //binding to a variable that indicates if matches are being retrieved asynchronously var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; //a callback executed when a match is selected var onSelectCallback = $parse(attrs.typeaheadOnSelect); var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; //INTERNAL VARIABLES //model setter executed upon match selection var $setModelValue = $parse(attrs.ngModel).assign; //expressions used by typeahead var parserResult = typeaheadParser.parse(attrs.cmcTypeahead); //pop-up element used to display matches var popUpEl = angular.element('<typeahead-popup></typeahead-popup>'); popUpEl.attr({ matches: 'matches', active: 'activeIdx', select: 'select(activeIdx)', query: 'query', position: 'position' }); //custom item template if(angular.isDefined(attrs.typeaheadTemplateUrl)) { popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); } //create a child scope for the typeahead directive so we are not polluting original scope //with typeahead-specific data (matches, query etc.) var scope = originalScope.$new(); originalScope.$on('$destroy', function() { scope.$destroy(); }); var resetMatches = function() { scope.matches = []; scope.activeIdx = -1; }; var getMatchesAsync = function(inputValue) { var matchParsePrefix = originalScope.$eval(attrs.typeaheadParsePrefix); var locals = { $viewValue: inputValue.indexOf(matchParsePrefix) === 0 ? inputValue.substring(matchParsePrefix.length, (inputValue.length + 1)) : inputValue }; isLoadingSetter(originalScope, true); $q.when(parserResult.source(scope, locals)).then(function(matches) { //it might happen that several async queries were in progress if a user were typing fast //but we are interested only in responses that correspond to the current view value //if(matches && inputValue === modelCtrl.$viewValue) /* Ehh.. that didn't seem to work when I "cleared" the input box */ if(matches) { if(matches.length > 0) { scope.activeIdx = 0; scope.matches.length = 0; //transform labels for(var i = 0; i < matches.length; i++) { locals[parserResult.itemName] = matches[i]; scope.matches.push({ label: parserResult.viewMapper(scope, locals), model: matches[i] }); } scope.query = inputValue; //position pop-up with matches - we need to re-calculate its position each time we are opening a window //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page //due to other elements being rendered scope.position = $position.position(element); scope.position.top = scope.position.top + element.prop('offsetHeight'); } else if(minSearch === 0) { resetMatches();//temp } else { resetMatches(); } isLoadingSetter(originalScope, false); } }, function() { resetMatches(); isLoadingSetter(originalScope, false); }); }; resetMatches(); /* Can't figure out how to make this work...*/ if(attrs.hasOwnProperty('typeaheadBindMatchReloader')) { $parse(attrs.typeaheadBindMatchReloader).assign(scope, function() { getMatchesAsync(element[0].value); }); } //we need to propagate user's query so we can higlight matches scope.query = undefined; //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later var timeoutPromise; //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue modelCtrl.$parsers.unshift(function(inputValue) { resetMatches(); if((inputValue && inputValue.length >= minSearch) || minSearch === 0) { if(waitTime > 0) { if(timeoutPromise) { $timeout.cancel(timeoutPromise);//cancel previous timeout } timeoutPromise = $timeout(function() { getMatchesAsync(inputValue); }, waitTime); } else { getMatchesAsync(inputValue); } } if(isEditable) { return inputValue; } else { modelCtrl.$setValidity('editable', false); return undefined; } }); modelCtrl.$formatters.push(function(modelValue) { var candidateViewValue, emptyViewValue; var locals = {}; if(inputFormatter) { locals['$model'] = modelValue; return inputFormatter(originalScope, locals); } else { //it might happen that we don't have enough info to properly render input value //we need to check for this situation and simply return model value if we can't apply custom formatting locals[parserResult.itemName] = modelValue; candidateViewValue = parserResult.viewMapper(originalScope, locals); locals[parserResult.itemName] = undefined; emptyViewValue = parserResult.viewMapper(originalScope, locals); return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue; } }); scope.select = function(activeIdx) { //called from within the $digest() cycle var locals = {}; var model, item; locals[parserResult.itemName] = item = scope.matches[activeIdx].model; model = parserResult.modelMapper(originalScope, locals); $setModelValue(originalScope, model); modelCtrl.$setValidity('editable', true); onSelectCallback(originalScope, { $item: item, $model: model, $label: parserResult.viewMapper(originalScope, locals) }); resetMatches(); //return focus to the input element if a mach was selected via a mouse click event element[0].focus(); }; //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) element.bind('keydown', function(evt) { //typeahead is open and an "interesting" key was pressed if(scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) return; evt.preventDefault(); if(evt.which === 40) { scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; scope.$digest(); } else if(evt.which === 38) { scope.activeIdx = (scope.activeIdx ? scope.activeIdx : scope.matches.length) - 1; scope.$digest(); } else if(evt.which === 13 || evt.which === 9) { scope.$apply(function() { scope.select(scope.activeIdx); }); } else if(evt.which === 27) { evt.stopPropagation(); resetMatches(); scope.$digest(); } }); // Keep reference to click handler to unbind it. var dismissClickHandler = function(evt) { if(element[0] !== evt.target) { resetMatches(); scope.$digest(); } else { getMatchesAsync(element[0].value); } }; $document.bind('click', dismissClickHandler); originalScope.$on('$destroy', function() { $document.unbind('click', dismissClickHandler); }); element.after($compile(popUpEl)(scope)); } }; }]); 

Appel à l'action:

Quelqu'un VEUILLEZ faire un exemple pratique de cette directive typeahead ! Je serais toujours endetté envers vous! (Eh bien, pas vraiment, mais cela me rendrait très heureux)

AVERTISSEMENT:

Je comprends que cette réponse n'est en aucun cas orthodoxe. Je n'ai pas fourni le askee (askee?) Avec une réponse directe à la question, mais j'ai fourni les outils dont je crois être nécessaires pour répondre à sa réponse. Je comprends que je devrais passer le temps de faire un exemple de travail, mais je suis un homme très occupé et j'ai simplement souhaité partager mon travail avec la communauté, car j'ai vu cette question trop souvent pendant que je me suis assis et que je tiens la réponse . Veuillez me faire savoir si vous avez des problèmes, des questions ou des complications. Je suis heureux d'aider.

Merci!