Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a AngularJS jQueryUI Autocomplete directive

I am trying to create a custom directive that uses jQueryUI's autocomplete widget. I want this to be as declarative as possible. This is the desired markup:

<div>
    <autocomplete ng-model="employeeId" url="/api/EmployeeFinder" label="{{firstName}} {{surname}}" value="id" />
</div>

So, in the example above, I want the directive to do an AJAX call to the url specified, and when the data is returned, show the value calculated from the expression(s) from the result in the textbox and set the id property to the employeeId. This is my attempt at the directive.

app.directive('autocomplete', function ($http) {
    return {
        restrict: 'E',
        replace: true,
        template: '<input type="text" />',
        require: 'ngModel',

        link: function (scope, elem, attrs, ctrl) {
            elem.autocomplete({
                source: function (request, response) {
                    $http({
                    url: attrs.url,
                    method: 'GET',
                    params: { term: request.term }
                })
                .then(function (data) {
                    response($.map(data, function (item) {
                        var result = {};

                        result.label = item[attrs.label];
                        result.value = item[attrs.value];

                        return result;
                    }))
                });
                },

                select: function (event, ui) {                    
                    ctrl.$setViewValue(elem.val(ui.item.label));                    

                    return false;
                }
            });
        }
    }    
});

So, I have two issues - how to evaluate the expressions in the label attribute and how to set the property from the value attribute to the ngModel on my scope.

like image 859
markp Avatar asked Mar 19 '23 08:03

markp


2 Answers

Here's my updated directive

(function () {
'use strict';

angular
    .module('app')
    .directive('myAutocomplete', myAutocomplete);

myAutocomplete.$inject = ['$http', '$interpolate', '$parse'];
function myAutocomplete($http, $interpolate, $parse) {

    // Usage:

    //  For a simple array of items
    //  <input type="text" class="form-control" my-autocomplete url="/some/url" ng-model="criteria.employeeNumber"  />

    //  For a simple array of items, with option to allow custom entries
    //  <input type="text" class="form-control" my-autocomplete url="/some/url" allow-custom-entry="true" ng-model="criteria.employeeNumber"  />

    //  For an array of objects, the label attribute accepts an expression.  NgModel is set to the selected object.
    //  <input type="text" class="form-control" my-autocomplete url="/some/url" label="{{lastName}}, {{firstName}} ({{username}})" ng-model="criteria.employeeNumber"  />

    //  Setting the value attribute will set the value of NgModel to be the property of the selected object.
    //  <input type="text" class="form-control" my-autocomplete url="/some/url" label="{{lastName}}, {{firstName}} ({{username}})" value="id" ng-model="criteria.employeeNumber"  />

    var directive = {            
        restrict: 'A',
        require: 'ngModel',
        compile: compile
    };

    return directive;

    function compile(elem, attrs) {
        var modelAccessor = $parse(attrs.ngModel),
            labelExpression = attrs.label;

        return function (scope, element, attrs) {
            var
                mappedItems = null,
                allowCustomEntry = attrs.allowCustomEntry || false;

            element.autocomplete({
                source: function (request, response) {
                    $http({
                        url: attrs.url,
                        method: 'GET',
                        params: { term: request.term }
                    })
                    .success(function (data) {
                        mappedItems = $.map(data, function (item) {
                            var result = {};

                            if (typeof item === 'string') {
                                result.label = item;
                                result.value = item;

                                return result;
                            }

                            result.label = $interpolate(labelExpression)(item);

                            if (attrs.value) {
                                result.value = item[attrs.value];
                            }
                            else {
                                result.value = item;
                            }

                            return result;
                        });

                        return response(mappedItems);
                    });
                },

                select: function (event, ui) {
                    scope.$apply(function (scope) {
                        modelAccessor.assign(scope, ui.item.value);
                    });

                    if (attrs.onSelect) {
                        scope.$apply(attrs.onSelect);
                    }

                    element.val(ui.item.label);

                    event.preventDefault();
                },

                change: function () {
                    var
                        currentValue = element.val(),
                        matchingItem = null;

                    if (allowCustomEntry) {
                        return;
                    }

                    if (mappedItems) {
                        for (var i = 0; i < mappedItems.length; i++) {
                            if (mappedItems[i].label === currentValue) {
                                matchingItem = mappedItems[i].label;
                                break;
                            }
                        }
                    }

                    if (!matchingItem) {
                        scope.$apply(function (scope) {
                            modelAccessor.assign(scope, null);
                        });
                    }
                }
            });
        };
    }
}
})();
like image 114
markp Avatar answered Mar 21 '23 21:03

markp


Sorry to wake this up... It's a nice solution, but it does not support ng-repeat...

I'm currently debugging it, but I'm not experienced enough with Angular yet :)

EDIT: Found the problem. elem.autocomplete pointed to elem parameter being sent into compile function. IT needed to point to the element parameter in the returning linking function. This is due to the cloning of elements done by ng-repeat. Here is the corrected code:

app.directive('autocomplete', function ($http, $interpolate, $parse) {
return {
    restrict: 'E',
    replace: true,
    template: '<input type="text" />',
    require: 'ngModel',

    compile: function (elem, attrs) {
        var modelAccessor = $parse(attrs.ngModel),
            labelExpression = attrs.label;

        return function (scope, element, attrs, controller) {
            var
                mappedItems = null,
                allowCustomEntry = attrs.allowCustomEntry || false;

            element.autocomplete({
                source: function (request, response) {
                    $http({
                        url: attrs.url,
                        method: 'GET',
                        params: { term: request.term }
                    })
                    .success(function (data) {
                        mappedItems = $.map(data, function (item) {
                            var result = {};                                    

                            if (typeof item === "string") {
                                result.label = item;
                                result.value = item;

                                return result;
                            }

                            result.label = $interpolate(labelExpression)(item);

                            if (attrs.value) {
                                result.value = item[attrs.value];
                            }
                            else {
                                result.value = item;
                            }

                            return result;
                        });

                        return response(mappedItems);
                    });
                },

                select: function (event, ui) {
                    scope.$apply(function (scope) {
                        modelAccessor.assign(scope, ui.item.value);
                    });

                    elem.val(ui.item.label);

                    event.preventDefault();
                },

                change: function (event, ui) {
                    var
                        currentValue = elem.val(),
                        matchingItem = null;

                    if (allowCustomEntry) {
                        return;
                    }

                    for (var i = 0; i < mappedItems.length; i++) {
                        if (mappedItems[i].label === currentValue) {
                            matchingItem = mappedItems[i].label;
                            break;
                        }
                    }                        

                    if (!matchingItem) {
                        scope.$apply(function (scope) {
                            modelAccessor.assign(scope, null);
                        });
                    }
                }
            });
        }
    }
}
});
like image 31
palaslet Avatar answered Mar 21 '23 22:03

palaslet