AngularJS Show typeahead on button click

前端 未结 1 998
轻奢々
轻奢々 2021-01-05 07:28

I am using the typeahead directive in AngularJS and it works fine. However, I would like to have a button outside of the input that when clicked would show the typeahead dro

1条回答
  •  轮回少年
    2021-01-05 08:10

    Ok, I am having an absolutely terrible time trying to create a JSFiddle or even a Plunkr for this, so I will just give you the code for this directive.

    This directive originally comes from..

    This epic Bootstrap library!

    ..and I stole it and played with it. If you would like to use it, you will need the "Bootstrap" (its really a subset of angular directives) library that I linked to. You can make your own subset of this library, but I am not entirely sure of all of the dependencies my directive has as I am using the entire library in my project. Basically, you need any directive that starts with "typeahead".

    As you can see, I have named the directive wwTypeahead (that "ww" is for WebWanderer!). It is a very easy to use directive and it works just like the original.

    
    

    The really important part to note is the attribute typeahead-min-length="0" which has really been the heart of many discussions online. I managed to make that work.

    This directive is meant to take the place of the typeahead directive in the library I linked to. Your typeahead list will be shown on focus of your input box. No, the list does not show on the click of a button, but hopefully getting there will be baby-steps from here. If you need help implementing that, I will be happy to help.

    /*
        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('');
                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));
            }
        };
    }]);
    

    Call To Action:

    Somebody PLEASE make a working example of this typeahead directive! I would forever be in debt to you! (well, not really but it would make me very happy)

    DISCLAIMER:

    I understand that this answer is in no way orthodox. I did not provide the askee (askee?) with a direct answer to the question, yet I did provide the tools that I believe are needed to get to his/her answer. I understand that I should spend the time to make a working example, but I am a very busy man and simply wished to share my work with the community, as I have seen this question asked too many times while I sit back and hold the answer. Please let me know if you have any issues, questions, or complications. I am happy to help.

    Thanks!

    0 讨论(0)
提交回复
热议问题