2017-07-18 39 views
15

problem

mam pola kombi zasadzie select element, który jest wypełniony za pomocą szeregu obiektów złożonych przez ng-options. Gdy aktualizuję dowolny obiekt z kolekcji na drugim poziomie, zmiana ta nie jest stosowana do pola kombi.ponownie czyni ng opcje po zmianie drugiego poziomu poboru

Jest również documented na stronie internetowej angularjs:

Zauważ, że $watchCollection robi płytkie porównanie właściwości przedmiotu (lub przedmiotów w kolekcji, jeśli model jest tablicą). Oznacza to, że zmiana właściwości głębiej niż pierwszy poziom w obiekcie/kolekcji nie spowoduje ponownego renderowania.

kątowe widok

<div ng-app="testApp"> 
    <div ng-controller="Ctrl"> 
     <select ng-model="selectedOption" 
       ng-options="(selectedOption.id + ' - ' + selectedOption.name) for selectedOption in myCollection track by selectedOption.id"> 
     </select> 
     <button ng-click="changeFirstLevel()">Change first level</button> 
     <button ng-click="changeSecondLevel()">Change second level</button> 
     <p>Collection: {{ myCollection }}</p> 
     <p>Selected: {{ selectedOption }}</p> 
    </div> 
</div> 

kątowa kontroler

var testApp = angular.module('testApp', []); 

testApp.controller('Ctrl', ['$scope', function ($scope) { 

    $scope.myCollection = [ 
     { 
      id: '1', 
      name: 'name1', 
      nested: { 
       value: 'nested1' 
      } 
     } 
    ]; 

    $scope.changeFirstLevel = function() { 
     var newElem = { 
      id: '1', 
      name: 'newName1', 
      nested: { 
       value: 'newNested1' 
      } 
     }; 
     $scope.myCollection[0] = newElem; 
    }; 

    $scope.changeSecondLevel = function() { 
     var newElem = { 
      id: '1', 
      name: 'name1', 
      nested: { 
       value: 'newNested1' 
      } 
     }; 
     $scope.myCollection[0] = newElem; 
    }; 

}]); 

Można również uruchomić go na żywo w this JSFiddle.

Pytanie

rozumiem, że angularjs nie oglądać skomplikowanych obiektów w ng-options ze względu na wydajność. Ale czy istnieje sposób obejścia tego problemu, tj. Czy mogę ręcznie uruchomić ponowne renderowanie? Niektóre posty wspominają o rozwiązaniu jako $timeout lub $scope.apply, ale nie mogłem ich wykorzystać.

+0

Możesz po prostu umieścić $ scope.selectedOption = newElem; wewnątrz funkcji $ scope.changeSecondLevel() – Vivz

+0

Nie, chcę zaktualizować kolekcję. Ustawienie wybranego elementu bezpośrednio nie jest opcją. W mojej aplikacji '' changeSecondLevel() '' wykonuje żądanie HTTP, które zwraca cały zestaw nowych elementów, które ponownie przypisuję do '' myCollection''. Właśnie wybrałem tutaj jeden element dla uproszczenia. – user1438038

+0

Czy w Twojej aplikacji wartość 'id' jest unikatowa w' myCollection'? A kiedy mówisz, że zmieniasz przypisanie 'myCollection', czy robisz' myCollection = ... 'lub czy przeglądasz listę jak w twoim przykładzie i czy' myCollection [i] = ... '? – user2718281

Odpowiedz

3

Tak, to jest trochę brzydkie i wymaga brzydkiej pracy.

Rozwiązanie $timeout działa, dając AngularJS zmianę, aby rozpoznać, że płytkie właściwości uległy zmianie w bieżącym cyklu wytrawiania, jeśli ustawiono tę kolekcję na [].

Przy następnej okazji, poprzez $timeout, ustawiasz ją z powrotem w to, co było, a AngularJS rozpoznaje, że płytki właściwości zmieniły się na coś nowego i odpowiednio zaktualizuje swój ngOptions.

Inną rzeczą, którą dodałem w wersji demonstracyjnej, jest zapisanie bieżącego identyfikatora przed aktualizacją kolekcji. Następnie można go użyć do ponownego wyboru tej opcji, gdy kod $timeout przywraca kolekcję (zaktualizowaną).

Demo: http://jsfiddle.net/4639yxpf/

var testApp = angular.module('testApp', []); 

testApp.controller('Ctrl', ['$scope', '$timeout', function($scope, $timeout) { 

    $scope.myCollection = [{ 
    id: '1', 
    name: 'name1', 
    nested: { 
     value: 'nested1' 
    } 
    }]; 

    $scope.changeFirstLevel = function() { 
    var newElem = { 
     id: '1', 
     name: 'newName1', 
     nested: { 
     value: 'newNested1' 
     } 
    }; 
    $scope.myCollection[0] = newElem; 
    }; 

    $scope.changeSecondLevel = function() { 

    // Stores value for currently selected index. 
    var currentlySelected = -1; 

    // get the currently selected index - provided something is selected. 
    if ($scope.selectedOption) { 
     $scope.myCollection.some(function(obj, i) { 
     return obj.id === $scope.selectedOption.id ? currentlySelected = i : false; 
     }); 
    } 

    var newElem = { 
     id: '1', 
     name: 'name1', 
     nested: { 
     value: 'newNested1' 
     } 
    }; 
    $scope.myCollection[0] = newElem; 

    var temp = $scope.myCollection; // store reference to updated collection 
    $scope.myCollection = []; // change the collection in this digest cycle so ngOptions can detect the change 

    $timeout(function() { 
     $scope.myCollection = temp; 
     // re-select the old selection if it was present 
     if (currentlySelected !== -1) $scope.selectedOption = $scope.myCollection[currentlySelected]; 
    }, 0); 
    }; 

}]); 
+0

To wydaje się najbardziej odpowiednie rozwiązanie sugerowane do tej pory. Wciąż brzydkie obejście, jestem tego świadomy, mechanizm ten wychwyci wszystkie zmiany, bez względu na to, na jakim poziomie się znajdują. Zasadniczo spowoduje to ponowne renderowanie całej kolekcji. – user1438038

+0

Zastanawiam się jednak, czy ma to jakiś wpływ na wydajność? Odwołanie do kolekcji jest przechowywane w zmiennej tymczasowej, a następnie przywracane. Dla mnie brzmi to jak tanią operację. A może to wywoła w tle jakiekolwiek drogie rutyny w ramach AngularJS? – user1438038

+1

Jest to operacja o niskiej cenie, jak można to ująć, ponieważ JavaScript przesyła jedynie kopie wskaźnika, a nie kopie obiektu. Angular ma zamiar zrobić płytkie porównanie niezależnie od tego, o ile widzę, jego koszt jest taki sam, jak dwukrotna zmiana na pierwszy poziom ... nic znaczącego w ogóle –

3

Wyjaśnienie dlaczego changeFirstLevel działa

Używasz (selectedOption.id + ' - ' + selectedOption.name) ekspresji do renderowania wybierz Opcje etykiet. Oznacza to, że wyrażenie {{selectedOption.id + ' - ' + selectedOption.name}} działa dla etykiety elementów wybranych. Po wywołaniu changeFirstLevel func name z selectedOption zmienia się z name1 na newName1. Z tego powodu html jest odtwarzany pośrednio.

Rozwiązanie 1

Jeżeli wydajność nie jest dla Ciebie problem można po prostu usunąć wyrażenie track by i problem zostanie rozwiązany. Ale jeśli chcesz wydajności i ponownego wydania w tym samym czasie obie będą nieco niskie.

Rozwiązanie 2

Dyrektywa ta jest głęboko obserwując zmiany i zastosować go do model.

var testApp = angular.module('testApp', []); 
 

 
testApp.directive('collectionTracker', function(){ 
 

 
return { 
 
\t restrict: 'A', 
 
    require: 'ngModel', 
 
    link: function(scope, element, attrs, ngModel) { 
 
     var oldCollection = [], newCollection = [], ngOptionCollection; 
 
\t \t 
 
    
 
     scope.$watch(
 
     function(){ return ngModel.$modelValue }, 
 
     function(newValue, oldValue){ 
 
     \t if(newValue != oldValue ) 
 
     { 
 
     
 
     
 
     
 
      for(var i = 0; i < ngOptionCollection.length; i++) 
 
      { 
 
     \t //console.log(i,newValue,ngOptionCollection[i]); 
 
     \t if(angular.equals(ngOptionCollection[i] , newValue)) 
 
     \t { 
 
      \t \t 
 
      \t \t newCollection = scope[attrs.collectionTracker]; 
 
        setCollectionModel(i); 
 
        ngModel.$setUntouched(); 
 
        break; 
 
       } 
 
      } 
 
     \t 
 
     } 
 
     }, true); 
 
    
 
     scope.$watch(attrs.collectionTracker, function(newValue, oldValue) 
 
     { 
 
    \t 
 
      if(newValue != oldValue) 
 
    \t  { 
 
     \t  newCollection = newValue; 
 
       oldCollection = oldValue; 
 
       setCollectionModel(); 
 
      } 
 
    \t 
 
     }, true) 
 
    
 
     scope.$watch(attrs.collectionTracker, function(newValue, oldValue){ 
 
    \t 
 
      if(newValue != oldValue || ngOptionCollection == undefined) 
 
    \t { 
 
     \t  //console.log(newValue); 
 
     \t  ngOptionCollection = angular.copy(newValue); 
 
      } 
 
    \t 
 
     }); 
 
    
 
    
 
    
 
     function setCollectionModel(index) 
 
     { 
 
    \t var oldIndex = -1; 
 
     
 
      if(index == undefined) 
 
      { 
 
     \t  for(var i = 0; i < oldCollection.length; i++) 
 
       { 
 
     \t  if(angular.equals(oldCollection[i] , ngModel.$modelValue)) 
 
     \t  { 
 
        oldIndex = i; 
 
        break; 
 
        } 
 
       } 
 
     
 
      } 
 
      else 
 
     \t  oldIndex = index; 
 
     
 
\t \t \t //console.log(oldIndex); 
 
\t \t \t ngModel.$setViewValue(newCollection[oldIndex]); 
 
    
 
     } 
 
    }} 
 
}); 
 

 
testApp.controller('Ctrl', ['$scope', function ($scope) { 
 

 
    $scope.myCollection = [ 
 
     { 
 
      id: '1', 
 
      name: 'name1', 
 
      nested: { 
 
      \t value: 'nested1' 
 
      } 
 
     }, 
 
     { 
 
      id: '2', 
 
      name: 'name2', 
 
      nested: { 
 
      \t value: 'nested2' 
 
      } 
 
     }, 
 
     { 
 
      id: '3', 
 
      name: 'name3', 
 
      nested: { 
 
      \t value: 'nested3' 
 
      } 
 
     } 
 
    ]; 
 
    
 
    $scope.changeFirstLevel = function() { 
 
     var newElem = { 
 
      id: '1', 
 
      name: 'name1', 
 
      nested: { 
 
      \t value: 'newNested1' 
 
      } 
 
     }; 
 
    \t $scope.myCollection[0] = newElem; 
 
    }; 
 

 
    $scope.changeSecondLevel = function() { 
 
     var newElem = { 
 
      id: '1', 
 
      name: 'name1', 
 
      nested: { 
 
      \t value: 'newNested2' 
 
      } 
 
     }; 
 
    \t $scope.myCollection[0] = newElem; 
 
    }; 
 
    
 

 
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.5/angular.min.js"></script> 
 
<div ng-app="testApp"> 
 
    <div ng-controller="Ctrl"> 
 
     <p>Select item 1, then change first level. -> Change is applied.</p> 
 
     <p>Reload page.</p> 
 
     <p>Select item 1, then change second level. -> Change is not applied.</p> 
 
     <select ng-model="selectedOption" 
 
       collection-tracker="myCollection" 
 
       ng-options="(selectedOption.id + ' - ' + selectedOption.name) for selectedOption in myCollection track by selectedOption.id"> 
 
     </select> 
 
     <button ng-click="changeFirstLevel()">Change first level</button> 
 
     <button ng-click="changeSecondLevel()">Change second level</button> 
 
     <p>Collection: {{ myCollection }}</p> 
 
     <p>Selected: {{ selectedOption }}</p> 
 
    </div> 
 
</div>

5

Szybkie Hack Użyłem przed umieścić swoje wybierz wewnątrz ng-czy, ustaw ng-jeśli false, a następnie ustawić go na true po $ timeout równy 0. Spowoduje to, że kąt zostanie zreplikowany.

Alternatywnie możesz spróbować renderować opcje samodzielnie, używając powtórzenia ng. Nie jestem pewien, czy to zadziała.

+0

Chociaż może to zadziałać, brzmi jak bardzo brudny hack i raczej nie chcę tego robić. :) – user1438038

3

Dlaczego po prostu nie śledzisz kolekcji według tej zagnieżdżonej nieruchomości?

<select ng-model="selectedOption" 
      ng-options="(selectedOption.id + ' - ' + selectedOption.name) for selectedOption in myCollection track by selectedOption.nested.value"> 

Aktualizacja

Ponieważ nie wiem, która to właściwość śledzić można po prostu śledzić wszystkie właściwości przechodzących funkcję na torze przez ekspresję.

ng-options="(selectedOption.id + ' - ' + selectedOption.name) for selectedOption in myCollection track by $scope.optionsTracker(selectedOption)" 

A na kontrolerze:

$scope.optionsTracker = (item) => { 

    if (!item) return; 

    const firstLevelProperties = Object.keys(item).filter(p => !(typeof item[p] === 'object')); 
    const secondLevelProperties = Object.keys(item).filter(p => (typeof item[p] === 'object')); 
    let propertiesToTrack = ''; 

    //Similarilly you can cache any level property... 

    propertiesToTrack = firstLevelProperties.reduce((prev, curr) => { 
     return prev + item[curr]; 
    }, ''); 

    propertiesToTrack += secondLevelProperties.reduce((prev, curr) => { 

     const childrenProperties = Object.keys(item[curr]); 
     return prev + childrenProperties.reduce((p, c) => p + item[curr][c], ''); 

    }, '') 


    return propertiesToTrack; 
} 
+0

Istnieje wiele zagnieżdżonych właściwości i nie wiem z góry, który atrybut się zmieni. – user1438038

+1

Możesz rozważyć moją zaktualizowaną odpowiedź. – Korte

+2

Dziękuję za twój wysiłek, ale wolę bardziej lekkie rozwiązanie, które jest mniej dostosowane do konkretnej struktury obiektu. – user1438038

1

myślę, że każde rozwiązanie tutaj będzie albo overkill (nowa dyrektywa) lub nieco hack ($ timeout).

Ramy nie robią tego automatycznie z jakiegoś powodu, o którym już wiemy, że jest wydajnością. Podawanie kątowe do odświeżania byłoby generalnie mile widziane, imo.

Uważam więc, że najmniej inwazyjną zmianą byłoby dodanie metody ng-change i ustawienie jej ręcznie zamiast polegania na zmianie modelu ng. Nadal będziesz potrzebował modelu ng, ale od teraz będzie to obiekt atrapa. Twoja kolekcja zostanie przypisana po powrocie (.then) odpowiedzi, a tym bardziej po tym.

Więc na kontrolerze:

$scope.change = function(obj) { 
    $scope.selectedOption = obj; 
} 

a każda metoda kliknięcie przycisku przypisać do obiektu bezpośrednio:

$scope.selectedOption = newElem; 

zamiast

$scope.myCollection[0] = newElem; 

Na widzenia:

<select ng-model="obj" 
      ng-options="(selectedOption.id + ' - ' + selectedOption.name) for selectedOption in myCollection track by selectedOption.id" 
      ng-change="change(obj)"> 
    </select> 

Mam nadzieję, że to pomaga.

+0

Mówisz, że mówienie Angularowi, by odświeżyć, byłoby marszczeniem brwi. Czy możesz to rozwinąć? – user1438038

+0

Samo samodzielne zarządzanie zmianami nie jest dla mnie opcją, ponieważ moja metoda obsługi bardzo często zmienia wiele elementów kolekcji, które mogą mieć lub nie mieć wiele zagnieżdżonych atrybutów. – user1438038

+1

Używanie czegoś w rodzaju $ scope. $ Apply jest zwykle konieczne tylko wtedy, gdy mamy do czynienia z obiektem javascript, który znajduje się poza ramową strukturą lub obiektem jquery z innej biblioteki. Używanie czegoś takiego jak $ timeout pokazuje, że naprawdę nie wiemy, jak obchodzić się z obietnicami lub poprawnie obchodzić się z ramami za pomocą zegarków $ lub $ onChange, stąd moja opinia o tym jest niezadowolona. – WebDever