AngularJS在按钮单击时显示预先输入

时间:2015-07-22 13:23:17

标签: javascript angularjs twitter-bootstrap

我在AngularJS中使用typeahead指令,它工作正常。但是,我想在输入之外有一个按钮,当点击它时会显示预先输入下拉列表。这是我所追求的片段......

<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>

1 个答案:

答案 0 :(得分:1)

好的,我正在尝试为此创建一个JSFiddle甚至是一个Plunkr,所以我只会给你这个指令的代码。

该指令最初来自..

This epic Bootstrap library!

..我偷了它并玩它。如果你想使用它,你将需要&#34; Bootstrap&#34; (它实际上是角度指令的一个子集)我链接到的库。你可以自己创建这个库的子集,但是我并不完全确定我的指令所具有的所有依赖关系,因为我在项目中使用了整个库。基本上,您需要任何以&#34; typeahead&#34;。

开头的指令

正如您所看到的,我已将该指令命名为wwTypeahead(&#34; ww&#34;适用于WebWanderer!)。它是一个非常容易使用的指令,它的工作方式与原始指令相同。

<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" 
/>

值得注意的重要部分是属性typeahead-min-length="0",它确实是许多在线讨论的核心。我设法做到了这一点。

该指令旨在取代我链接到的库中的typeahead指令。您的输入列表将显示在输入框的focus上。不,单击按钮时列表不会显示,但希望从这里开始有婴儿步骤。如果您需要帮助实施,我将很乐意为您提供帮助。

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

行动呼吁:

有人为这个typeahead指令做一个有效的例子!我会永远欠你的债! (好吧,不是真的,但这会让我很开心)

<强>声明:

我明白这个答案绝不是正统的。我没有提供askee(askee?)直接回答这个问题,但我确实提供了我认为需要的工具来回答他/她的答案。我明白我应该花时间做一个工作的例子,但我是一个非常忙碌的人,只是希望与社区分享我的工作,因为我看到这个问题被问了太多次我坐下来拿着答案。如果您有任何问题,疑问或复杂情况,请与我们联系。我很乐意提供帮助。

谢谢!