Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular select binding breaking references in model

Angular version is 1.4.7.

The model in question contains two objects, 'systems', an array, and 'selectedSystem'. What I want is for selectedSystem to reference one of the objects in systems. This is the case when the page loads, and everything works as expected, but when I make a selection from the first dropdown, selectedSystem seems to become a copy rather than a reference to the original object in systems. Consequently, changes to the second drop down no longer are reflected in systems.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Hello</title>
</head>
<body ng-app="testApp">
    Hi There!
    <div ng-controller="TestAppSummaryCtrl">
        <input type="button" value="Add Query" ng-click="addQuery()"/>
        <select ng-model="state.selectedSystem" ng-options="system.description for system in state.systems track by system.systemId" ></select>
        <select ng-model="state.selectedSystem.currentEnvironment" ng-options="environment.description for environment in state.selectedSystem.environments track by environment.environmentId"></select>
        Selected System: {{state.selectedSystem.systemId}}

        <div ng-repeat="item in state.systems">
            System: {{item.description}}
            Current Environment: {{item.currentEnvironment.description}}
        </div>

        <div ng-repeat="item in state.selectedSystem.categories">
            Cateogry:
            {{item.categoryId}}
            {{item.description}}
            <br />
            Queries:
            <div ng-repeat="query in item.queries">
                {{query.queryId}}
                {{query.latestStatus}}
            </div>
        </div>
    </div>
    <script src="../Scripts/angular.js"></script>
    <script src="../Scripts/angular-route.js"></script>
    <script src="src/test.js"></script>
</body>
</html>

typescript code:

/// <reference path="../../scripts/typings/angularjs/angular.d.ts" />
/// <reference path="../../scripts/typings/angularjs/angular-route.d.ts" />

module TestApp {
    export class Config {
        constructor($routeProvider: ng.route.IRouteProvider) {
            $routeProvider.when("/test", {
                templateUrl: "StaticContent/StaticTest.html",
                controller: "TestAppCtrl"
            });
        }
    }

    Config.$inject = ['$routeProvider'];

    export class SummaryService {
        private summaryApiPath: string;
        private httpService: ng.IHttpService;
        private qService: ng.IQService;
        private systems: Array<Extensions.SystemSummary>;

        constructor($http: ng.IHttpService, $q: ng.IQService) {
            this.summaryApiPath = "../api/systemList";
            this.httpService = $http;
            this.qService = $q;
        }

        getSystems(): ng.IPromise<any> {
            if (this.systems != undefined) {
                return this.qService.when(this.systems);
            }
            var deferred = this.qService.defer();
            this.httpService.get(this.summaryApiPath).then((result: any) => {
                deferred.resolve(result.data);
            }), error => {
                deferred.reject(error);
            }
            return deferred.promise;
        }

        public static serviceFactory($http: ng.IHttpService, $q: ng.IQService): SummaryService {
            return new SummaryService($http, $q);
        }
    }

    export class TestAppSummaryCtrl {
        private $scope: Extensions.ISummaryScope
        private summaryService: SummaryService;

        private init(): void {
            var local = this.$scope;
            this.summaryService.getSystems().then(data => {
                local.state.systems = <Array<Extensions.SystemSummary>>data;
                local.state.selectedSystem = local.state.systems.length == 0 ? undefined : local.state.systems[0];
            });
            local.updateCurrentEnvironment = envId => local.state.selectedSystem.currentEnvironment = local.state.selectedSystem.environments[envId];
        }

        constructor($scope: Extensions.ISummaryScope, summaryService: SummaryService) {
            this.$scope = $scope;
            this.$scope.state = new Extensions.SummaryCtrlUIState();
            this.summaryService = summaryService;
            this.init();
        }
    }

    TestAppSummaryCtrl.$inject = ['$scope', 'summaryService'];

    var app = angular.module('testApp', ['ngRoute']);
    app.config(Config);
    app.factory('summaryService', ['$http', '$q', SummaryService.serviceFactory]);
    app.controller('TestAppSummaryCtrl', TestAppSummaryCtrl);
}

module Extensions {
    export class CategorySummary {
        categoryId: number;
        description: number;
        queries: Array<JobItemSummary>;
    }

    export class JobItemSummary {
        queryId: number;
        lastJobId: number;
        lastCompletedDate: string;
        latestStatus: string;
        latestResultsCount: number;
        latestResultsSummary: string;
        expectedResult: number;
    }

    export class EnvironmentSummary {
        environmentId: number;
        description: string;
    }

    export class SystemSummary {
        systemId: number;
        description: string;
        environments: Array<EnvironmentSummary>;
        currentEnvironment: EnvironmentSummary;
        categories: Array<CategorySummary>;
    }

    export class SummaryCtrlUIState {
        selectedSystem: Extensions.SystemSummary;
        systems: Array<Extensions.SystemSummary>;
    }

    export interface ISummaryScope extends ng.IScope {

        state: SummaryCtrlUIState;
        updateCurrentEnvironment(envId: number): void;
        addQuery(): void; 
    }
}

What is going on, and Is there some way to get the behavior I want with angular model binding?

like image 444
austior Avatar asked Apr 14 '26 00:04

austior


1 Answers

Greetings from the future.

Having a similar issue as you, and since this is over a year old, I figured I'd do some digging. This seems to be by design, even in v1.6. Lines 399-403 of ngOptions has this interesting tidbit:

        getViewValueFromOption: function(option) {
            // If the viewValue could be an object that may be mutated by the application,
            // we need to make a copy and not return the reference to the value on the option.
            return trackBy ? copy(option.viewValue) : option.viewValue;
        }

So yeah, track by is making a copy of the value to your ngModel. For now I think the only recourse would be to write your own logic to track selections and/or send changes back to the originating array.

edit: I've opened an issue on the angular.js github repo: https://github.com/angular/angular.js/issues/15980

like image 126
p0lar_bear Avatar answered Apr 16 '26 14:04

p0lar_bear



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!