Angularjs Custom select2 directive
Asked Answered
O

5

18

I have created simple custom AngularJs directive for this awesome jquery plugin jQuery-Select2 as follows:

Directive

app.directive("select2",function($timeout,$parse){
    return {
        restrict: 'AC',
        link: function(scope, element, attrs) {
            $timeout(function() {
                $(element).select2();
            },200); 
        }
    };
});

Usage in HTML templates:

<select class="form-control" select2 name="country"
data-ng-model="client.primary_address.country"
ng-options="c.name as c.name for c in client.countries">
     <option value="">Select Country</option>
</select>

It is working as expected and my normal select element is replaced by select2 plugins.

However there is one issue though, sometimes it is showing default value i.e Select Country here although in dropdown proper model value is auto selected.

Now if I increase $timeout interval from 200 to some high value say 1500, it is working but delays the the rendering of directive. Also I think this is not proper solution for it, as my data is getting loaded via ajax.

I have also tried to update directive as follows, but no luck in that either:

app.directive("select2",function($timeout,$parse){
    return {
        restrict: 'AC',
        require: 'ngModel',
        link: function(scope, element, attrs) {
            var modelAccessor = $parse(attrs.ngModel);
            $timeout(function() {
                $(element).select2();
            });
            scope.$watch(modelAccessor, function (val) {
                if(val) {
                    $(element).select2("val",val);
                }
            });
        }
    };
});

PS: I know that there is similar module present ui-select, but it requires some different markup in form of <ui-select></ui-select>, and my App is already fully developed and I just want to replace normal select box with select2.

So can you please guide me how can I resolve this issue and make sure that directive keeps in sync with latest behaviour?

Orvieto answered 15/4, 2015 at 7:40 Comment(8)
Is this anything to do with select2? If you remove the select2 directive, and make it a normal select element, does it work as expected?Flexor
Yes it it working as expected if I remove so.Orvieto
I'm also using select2 in my application but I'm using ui-select2 which is Angular's wrapper for it, which is now deprecated. Select2 has caused me a lot of grief btw, I suggest you avoid it if you can :)Powel
@OmriAharon using ui-select2 is better thing rather than creating own library which has already written..Incus
@pankajparkar Yes, I'm suggesting to use the recommend ui-select.Powel
Guys, I know it's better to use ui-select. But I can not do it now, as my app is developed now and I don't want to replace every select boxes with ui-select.Orvieto
You can broadcast en event in controller, and catch it up in directive scope to reinitialize the select2 plugin.Canteen
I would normally say yes, go with ui-select2 (deprecated), now ui-select2, I'm in the same pickle jar, but it's been a while since they updated it. They are getting more and more behind in bootstrap css styling.. then finding out using older versions of the libraries they are depending on, would help. I think if you create your own version you have way more control over your component and you can maintain and update when necessary. I look at the ui-select, and they are quite a few months behind since their last updateNubbin
R
8

It might be simpler than you expected!

Please have a look at this Plunker

Basically, all plugins, Angularjs $watch need to be based on something. I'm not 100% sure for jQuery-select2; but I think that's just the control's normal DOM events. (And in the case of Angular $watch, it is a "dirty checking loop")

My idea is that let's trust jquery-Select2 and AngularJS for handling those change events.

We just need to watch for change in Angular's ways and update the select in Select2's ways

var refreshSelect = function() {
    if (!element.select2Initialized) return;
    $timeout(function() {
        element.trigger('change');
    });
};

//...

scope.$watch(attrs.ngModel, refreshSelect);

Notice: I have added in 2 new watch which I think you would like to have!

Redman answered 30/4, 2015 at 23:47 Comment(4)
With control as approach this wont work, can you make it to work and include it in your answer? see this plunker changes in select is reflected in scope variable, but changes in scope variable is not reflected in selectLegislate
You have a typo mistake in your Plunkr. You should change the ng-click of the buttons to include cc. tooRedman
I would strongly suggest using 'change.select2' event rather than 'change'. I was always getting infinite loops of change events until I used undocumented event 'change.select2'Chard
This solution does not work when the model is an object.Barmen
C
4

I'm not that familiar with select2 (so the actual API for getting and setting the displayed value in the control may be incorrect), but I suggest this as an alternative:

app.directive("select2",function($timeout){
    return {
        restrict: 'AC',
        require: 'ngModel',
        link: function(scope, element, attrs, model) {

            $timeout(function() {
                element.select2();
            });

            model.$render = function() {
                element.select2("val",model.$viewValue);
            }
            element.on('change', function() {
                scope.$apply(function() {
                    model.$setViewValue(element.select2("val"));
                });
            })
        }
    };
});

The first $timeout is necessary because you are using ng-options, so the options won't be in the DOM until the next digest cycle. The problem with this is that new options won't be added to the control if the countries model is later changed by your application.

Culminate answered 27/4, 2015 at 23:55 Comment(0)
S
3

Angular is not going to like have model data modified by a third party plugin. My guess based on the fact that your using $timeout is there is a race condition between Angular updating the options or the model and the select2 plugin. The solution I came up with is to take the updating mostly out of Angular's hands and do it manually from the directive, that way you can ensure everything is matching no matter who is modifying. Here's the directive I came up with:

app.directive("select2",function($timeout,$parse){
    return {
        restrict: 'AC',
        link: function(scope, element, attrs) {
            var options = [],
                el = $(element),
                angularTriggeredChange = false,
                selectOptions = attrs["selectOptions"].split(" in "),
                property = selectOptions[0],
                optionsObject = selectOptions[1];
            // watch for changes to the defining data model
            scope.$watch(optionsObject, function(n, o){
                var data = [];
                // format the options for select2 data interface
                for(var i in n) {
                    var obj = {id: i, text: n[i][property]};
                    data.push(obj);
                }
                el.select2({data: data});
                // keep local copy of given options
                options = n;
            }, true);
            // watch for changes to the selection data model
            scope.$watch(attrs["selectSelection"], function(n, o) {
                // select2 is indexed by the array position,
                // so we iterate to find the right index
                for(var i in options) {
                    if(options[i][property] === n) {
                        angularTriggeredChange = true;
                        el.val(i).trigger("change");
                    }
                }
            }, true);
            // Watch for changes to the select UI
            el.select2().on("change", function(e){
                // if the user triggered the change, let angular know
                if(!angularTriggeredChange) { 
                    scope.$eval(attrs["selectSelection"]+"='"+options[e.target.value][property]+"'");
                    scope.$digest();
                }
                // if angular triggered the change, then nothing to update
                angularTriggeredChange = false;
            });

        }
    };
});

I've added to attributes select-options and select-model. These will be used to populate and update the data using select2's interface. Here is a sample html:

<select id="sel" class="form-control" select2 name="country"
  select-selection="client.primary_address.country" 
  select-options="name in client.countries" >
     <option value="">Select Country</option>
</select>
<div>Selected: {{client.primary_address.country}}</div>

Please note there's still some cleanup that could be done to the directive and there are any things at assumes about the input, such as the "in" in the select-options attribute. It also doesn't enforce the attributes but just fails if they don't exist.

Also please note that I've used Select2 version 4, as evidenced by the el.val(i).trigger("change"). You may have to revert some things if using an older version.

Here is the jsfiddle demo of directive in action.

Semantics answered 1/5, 2015 at 16:40 Comment(0)
R
1

It does not directly answer your question, but please take it as there are some people out there who wants to do another approach rather than sticking to jQuery select2.

I have built my own for this purpose because I was not satisfied the existing ones that are not exactly following Angular principles, HTML-first.

It's still early stage, but I think all features are working in all modern browsers.

https://github.com/allenhwkim/angular-autocomplete

These are examples

Refusal answered 30/4, 2015 at 20:34 Comment(0)
M
1

I tried to reproduce your issue and it seems to work well. here is the fiddle I came up with:

http://jsfiddle.net/s24gLdgq/

You may have a different behavior depending on the version of Angular and/or Select2 you are using, could you specify that?

Also if you want to prevent flickering, be sure to hide the default <select> tag so nothing is displayed before the select2 element pops out.

This is also done in my jsfiddle with the CSS

.form-control { width: 200px; opacity: 0 }
Mailbag answered 30/4, 2015 at 21:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.