AngularJS: ng-model switching int to string

thisisboris picture thisisboris · Jul 18, 2014 · Viewed 12.4k times · Source

I'm currently working on an app in Angular. So far, everything has been going -quite- well. I'm really, really new to angular and am amazed that it took so long for the first real roadblock.

Situation:

I have an array of objects each with an order.

category.items = [{id: 1, order: 1, type: {}, ...}, {id: 54, order: 2, type: {}, ...}, {id: 3, order: 3, type: {}, ...}]

The user needs to be able to rearrange these items. The new order must be set to the object property 'order'.

In html these objects are rendered like so:

<div class="category">
    <div class="item" ng-repeat="(itemIndex, item) in category.items track by $index">
        <div class="header">
        </div>
    </div>
</div>

In the header-div I have an inputfield, type select.

<select ng-model="item.order"  ng-change="changeItemOrder((itemIndex + 1), item.order, itemIndex)">
  <option ng-repeat="item in category.items" ng-value="($index + 1)">{{$index + 1}}</option>
</select>

The code for changeItemOrder:

$scope.changeItemOrder = function(old_order, new_order, item_index) {
    new_order = parseInt(new_order);
    if (old_order != new_order) {
        var upper = Math.max(old_order, new_order);
        var lower = Math.min(old_order, new_order);

        angular.forEach($scope.category.items, function(item, key) {
            if (item_index != key) {
                if (new_order < old_order) {
                    if (item_index >= new_order && (key + 1) >= lower && (key + 1) <= upper) {
                        item.order = (parseInt(item.order) + 1);
                    }
                } else if (new_order > old_order) {
                    if (item_index <= old_order && (key + 1) <= upper && (key + 1) >= lower) {
                        item.order = (parseInt(item.order) - 1);
                    }
                }
            } else {
                item.order = parseInt(new_order);
            }
        });

        $scope.reorderItems();
    }
};

(ReorderItems just call angular sorting with a default sorting mechanism comparing the orders and returning -1, 1 or 0.)

Here is where I discovered/spotted/pinpointed one of the breaking bugs in one of the possible solutions for this problem. Here I noticed that my INT is converted to string somehow, as on render an option is added to the dropdown with value 'string:2'.

I've tried ng-options, in all possible ways, but even those led to problems. The way I did ng-options was by doing item.order as item.order in ... and so on, that just made the order switch around until somehow all items had the same order. Trying different grouping methods or trackbys just gave different bugs, like suddenly introducing NaN en NULL in the dropdown, or completely removing the order property as a whole from the item-object.

So far the least bug-ridden solution has been using the ng-repeat on my options. That only causes a mismatch on the type of item.order.

Now, after searching far and wide, spending hours on stackoverflow (especially before writing up this question with that nifty little question-searching thingy) I come to you.

  1. How can I halt/circumvent the behavior where my item.order is switched from INT to STRING?

  2. If that isn't possible, how can I force my $index to be a string, so the model(string) matches the value(string)

  3. If that isn't possible, how can I write my ng-options so that I get the behavior I want? ( I've seriously tried a lot, from track by to different as and for statements, all resulted in different bugs)

    On initial load, all selects show a correct value selected, so all item.order are initially INT (I get them from our API), it's only after interacting that all but the object that triggered the reorder get messed up.

Answer

ivarni picture ivarni · Jul 18, 2014

It gets changed to a string because HTML has no concept of what an integer is. Ultimately the selected variables is read from the DOM and handed to angular and so it changes its type.

You can force the model to always contain an integer with a directive

directive('forceInt', function() {
  return {
    require: 'ngModel',
    link: function(scope, element, attrs, controller) {     
      controller.$parsers.push(function(value) {
        if (typeof value === 'string') {
          value = parseInt(value, 10);  
          controller.$setViewValue(value);
          controller.$render();
        }
        return value;
      });
    }
  };
});

(plunk)

But I see someone already pointed out that there might be better ways to keep track of the ordering. FWIW that directive will at least keep making sure the model is never a string.