select2, ng-model and angular
Asked Answered
R

1

5

Using jquery-select2 (not ui-select) and angular, I'm trying to set the value to the ng-model.

I've tried using $watch and ng-change, but none seem to fire after selecting an item with select2.

Unfortunately, I am using a purchased template and cannot use angular-ui.

HTML:

<input type="hidden" class="form-control select2remote input-medium"
    ng-model="contact.person.id"
    value="{{ contact.person.id }}"
    data-display-value="{{ contact.person.name }}"
    data-remote-search-url="api_post_person_search"
    data-remote-load-url="api_get_person"
    ng-change="updatePerson(contact, contact.person)">

ClientController:

$scope.updatePerson = function (contact, person) {
    console.log('ng change');
    console.log(contact);
    console.log(person);
} // not firing

$scope.$watch("client", function () {
    console.log($scope.client);
}, true); // not firing either

JQuery integration:

var handleSelect2RemoteSelection = function () {
    if ($().select2) {
        var $elements = $('input[type=hidden].select2remote');
        $elements.each(function(){
            var $this = $(this);
            if ($this.data('remote-search-url') && $this.data('remote-load-url')) {
                $this.select2({
                    placeholder: "Select",
                    allowClear: true,
                    minimumInputLength: 1,
                    ajax: { // instead of writing the function to execute the request we use Select2's convenient helper
                        url: Routing.generate($this.data('remote-search-url'), {'_format': 'json'}),
                        type: 'post',
                        dataType: 'json',
                        delay: 250,
                        data: function (term, page) {
                            return {
                                query: term, // search term
                            };
                        },
                        results: function (data, page) { // parse the results into the format expected by Select2.
                            return {
                                results: $.map(data, function (datum) {
                                    var result = {
                                        'id': datum.id,
                                        'text': datum.name
                                    };
                                    for (var prop in datum) {
                                        if (datum.hasOwnProperty(prop)) {
                                            result['data-' + prop] = datum[prop];
                                        }
                                    }
                                    return result;
                                })
                            }
                        }
                    },
                    initSelection: function (element, callback) {
                        // the input tag has a value attribute preloaded that points to a preselected movie's id
                        // this function resolves that id attribute to an object that select2 can render
                        // using its formatResult renderer - that way the movie name is shown preselected
                        var id = $(element).val(),
                            displayValue = $(element).data('display-value');
                        if (id && id !== "") {
                            if (displayValue && displayValue !== "") {
                                callback({'id': $(element).val(), 'text': $(element).data('display-value')});
                            } else {
                                $.ajax(Routing.generate($this.data('remote-load-url'), {'id': id, '_format': 'json'}), {
                                    dataType: "json"
                                }).done(function (data) {
                                    callback({'id': data.id, 'text': data.name});
                                });
                            }
                        }
                    },
                });
            }
        });
    }
};

Any advice would be greatly appreciated! :)

UPDATE

I've managed to put together a plunk which seems to similarly reproduce the problem - it now appears as if the ng-watch and the $watch events are fired only when first changing the value. Nevertheless, in my code (and when adding further complexity like dynamically adding and removing from the collection), it doesn't even seem to fire once.

Again, pointers in the right direction (or in any direction really) would be greatly appreciated!

Realism answered 26/4, 2015 at 17:27 Comment(10)
Can you share the jquery side of the integration? I suspect that you will need to do a little more work to handle the events emitted by jquery-select2.Ewen
What about trying to watch the input with a normal JavaScript (or jQuery) handler and using $scope.$apply, like $('.select2remote').on('change', function () { var value = $(this).value; $scope.$apply(function () { $scope.contact.person.id = value; }); })Aludel
@Aludel AFAIK $scope isn't available for me outside the module. Am I wrong?Realism
Your input field is inside a controller right? Write that code inside your controller function, otherwise what you want will never work.Aludel
Oh I see what you're asking.. anything outside of your module can be brought inside using $scope.$apply, that's pretty much exactly what it's for, but yea you have to write your jQuery event handler inside your controller where the $scope object exists.Aludel
I want to make sure I understand correctly - you suggest binding the event inside the controller? If so, this can't really work because I have multiple select2 elements on page, and I need to update the appropriate related model..Realism
Let us continue this discussion in chat.Aludel
I would recommend reproducing the problem in a plunk. First impressions is that the logic in handleSelect2RemoteSelection belongs in a directive, not a controller as a first step. Also, does jquery-select2 fire an onchange event on the element and modify the value of the input?Ewen
handleSelect2RemoteSelection is found in an external JS file (this is just how the template is.. I'm separating it bit by bit but I cannot re-do the whole thing, both for future upgrade purposes and time constraints). $('input[type=hidden].select2remote').on('change', function(){console.log('changed');}); does fire when appropriate. I'm having some trouble with mimicking the AJAX select2 behavior in jsfiddle (or plunk for that matter). Doing my best to get some working example ASAP :)Realism
@JoeEnzminger updated the question with the plunk linkRealism
E
7

There are a number of issues with your example. I'm not sure I am going to be able to provide an "answer", but hopefully the following suggestions and explanations will help you out.

First, you are "mixing" jQuery and Angular. In general, this really doesn't work. For example:

In script.js, you run

$(document).ready(function() {
  var $elements = $('input[type=hidden].select2remote');
  $elements.each(function() {
     //...
  });
});

This code is going to run once, when the DOM is initially ready. It will select hidden input elements with the select2remote class that are currently in the DOM and initialized the select2 plugin on them.

The problem is that any new input[type=hidden].select2remote elements added after this function is run will not be initialized at all. This would happen if you are loading data asynchronously and populating an ng-repeat, for example.

The fix is to move the select2 initialization code to a directive, and place this directive on each input element. Abridged, this directive might look like:

.directive('select2', function() {
    return {
        restrict: 'A',
        require: 'ngModel',
        link: function(scope, element, attr, ngModel) {

            //$this becomes element

            element.select2({
                //options removed for clarity
            });

            element.on('change', function() {
                 console.log('on change event');
                 var val = $(this).value;
                 scope.$apply(function(){
                     //will cause the ng-model to be updated.
                     ngModel.setViewValue(val);
                 });
            });
            ngModel.$render = function() {
                 //if this is called, the model was changed outside of select, and we need to set the value
                //not sure what the select2 api is, but something like:
                element.value = ngModel.$viewValue;
            }

        }
    }
});

I apologize that I'm not familiar enough with select2 to know the API for getting and setting the current value of the control. If you provide that to me in a comment I can modify the example.

Your markup would change to:

<input select2 type="hidden" class="form-control select2remote input-medium"
    ng-model="contact.person.id"
    value="{{ contact.person.id }}"
    data-display-value="{{ contact.person.name }}"
    data-remote-search-url="api_post_person_search"
    data-remote-load-url="api_get_person"
    ng-change="updatePerson(contact, contact.person)">

After implementing this directive, you could remove the entirety of script.js.

In your controller you have the following:

$('.select2remote').on('change', function () {
  console.log('change');
  var value = $(this).value;
  $scope.$apply(function () {
      $scope.contact.person.id = value;
  });
});

There are two problems here:

First, you are using jQuery in a controller, which you really shouldn't do.
Second, this line of code is going to fire a change event on every element with the select2remote class in the entire application that was in the DOM when the controller was instatiated.

It is likely that elements added by Angular (i.e through ng-repeat) will not have the change listener registered on them because they will be added to the DOM after the controller is instantiated (at the next digest cycle).

Also, elements outside the scope of the controller that have change events will modify the state of the controller's $scope. The solution to this, again, is to move this functionality into the directive and rely on ng-model functionality.

Remember that anytime you leave Angular's context (i.e if you are using jQuery's $.ajax functionality), you have to use scope.$apply() to reenter Angular's execution context.

I hope these suggestions help you out.

Ewen answered 27/4, 2015 at 23:45 Comment(2)
Just to complete the response here is the api for select2: $("#select").select2("val"); //get the value $("#select").select2("val", "foo"); //set the value so in this case just change element.value = ngModel.$viewValue by element.select2("val", ngModel.$viewValue)Ogawa
In my case, where the value of the select option is different from the text (view value), I have to use this: var newViewValue = $('option:selected', this).text(); scope.$apply(function(){ ngModel.$setViewValue(newViewValue); });Siphonophore

© 2022 - 2024 — McMap. All rights reserved.