AngularJS: ng-model switching int to string
Asked Answered
V

3

10

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.

Volturno answered 18/7, 2014 at 13:48 Comment(5)
Keeping track of order this way is really not necessary. In angular, the model is the source of truth. You already have the array of data - when you insert/remove items within the array, you are implicitly re-ordering the model.Karisa
@pixelbits, you're right, however, say I am to switch item at space 5 to space 2, how would you do that, split up the array at the point of insertion, put value in between and add them up again? Also, and this adds the difficulty, I need to be able to save the questions separately, then I do need the item.order. I could 'figure it out' on the go and push it to the object, but when doing that you might as well just keep it in and use it imo.Volturno
Use arr.splice to remove and insert the itemKarisa
Once the insert happens, send an update to the server at the same time with the new $index of the item. Or alternatively, send the entire array back to the server at the end.Karisa
@pixelbits, okay, I will suggest that to the powers that be, and probably solve it like that. I'll just have to change a few things serverside to make this work. Thanks a lot!Volturno
V
5

Eventually I was able to solve this by doing:

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

This, and only this solution (so far) has what I want/need. I need the item.order model to keep track of the current position and to show the correct value on initial load. You can't set the model to questionIndex because that disturbs other HTML-elements, I can't set it to $index because that also did weird things.

While the other solutions suggested here 'work', they do not result in the spec that was given to me. So I guess this is kinda specific.

Volturno answered 18/7, 2014 at 15:56 Comment(1)
I had the same issue in my project, and I fixed it by simply adding {{}} around my model. It solved my problem.Heteropterous
I
5

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.

Inspan answered 18/7, 2014 at 14:6 Comment(4)
I've tried this, it does not work in my situation, somehow it still thinks that my model is a string. If I select another value, the directive is called twice. I've placed a log before your if statement, The console output of 1 switch is: 'string' and 'number'. But the select remains blank, and an option is added '? string:5 ?'. So while your directive does work, in the end somehow when the select is created/changed it reads it as a string... Also it's the model that is a string, not the value of my options, if those were strings it would make sense with the DOM and everything...Volturno
Right, I guess I didn't fully understand your context. It will be run twice every time it changes the value, because changing the value triggers it to be run again. This is an awesome way of exhausting the stack if you're not careful. A directive like this still gives you the power to control what happens when the model changes (and with it's brother, the $formatter can provide a nice distinction between view and model). I guess it just wasn't the right tool this time around.Inspan
I've used this approach for a color-picker and a transparency-slider so it def. work in certain cases where you want control over the value, while not answering this question specifically I'm pretty sure that you solved quite a few others of people that turn up here via Google. (if that ever happens)Volturno
You'd be surprised how often people end up on SO when searching via Google. It's an awesome resource that has helped me countless times. Welcome to the site by the way, and I am happy to see you found a solution in the end.Inspan
V
5

Eventually I was able to solve this by doing:

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

This, and only this solution (so far) has what I want/need. I need the item.order model to keep track of the current position and to show the correct value on initial load. You can't set the model to questionIndex because that disturbs other HTML-elements, I can't set it to $index because that also did weird things.

While the other solutions suggested here 'work', they do not result in the spec that was given to me. So I guess this is kinda specific.

Volturno answered 18/7, 2014 at 15:56 Comment(1)
I had the same issue in my project, and I fixed it by simply adding {{}} around my model. It solved my problem.Heteropterous
T
0
.directive('numberToString', function () {
    return {
        require: 'ngModel',
        link: function (scope, element, attrs, ngModel) {
            ngModel.$parsers.push(function (value) {
                return '' + value;
            });
            ngModel.$formatters.push(function (value) {
                return value+'';
            });
        }
    }
});
Thrombus answered 9/7, 2016 at 17:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.