在集合中的第二级更改后重新渲染 ng-options
Posted
技术标签:
【中文标题】在集合中的第二级更改后重新渲染 ng-options【英文标题】:Re-render ng-options after second-level change in collection 【发布时间】:2017-12-23 00:09:42 【问题描述】:问题
我有一个组合框,基本上是一个select
元素,其中填充了ng-options
的复杂对象数组。当我在二级上更新集合的任何对象时,此更改不会应用于组合框。
这也是 AngularJS 网站上的documented:
请注意,
$watchCollection
对对象的属性(或者如果模型是数组,则为集合中的项)进行浅层比较。这意味着更改比对象/集合内的第一层更深的属性不会触发重新渲染。
角度视图
<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>
角度控制器
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;
;
]);
您也可以在 this JSFiddle 中实时运行它。
问题
我知道 AngularJS 出于性能原因不会监视 ng-options
中的复杂对象。 但是有什么解决方法可以解决这个问题,即我可以手动触发重新渲染吗?有些帖子提到$timeout
或$scope.apply
作为解决方案,但我都不能使用。
【问题讨论】:
你可以简单地把 $scope.selectedOption=newElem;在你的 $scope.changeSecondLevel() 函数中 不,我想更新收藏。直接设置所选元素不是一种选择。在我的应用程序中,changeSecondLevel()
执行一个 HTTP 请求,该请求返回一整套我重新分配给 myCollection
的新元素。为了简单起见,我在这里只选择了一个元素。
在您的应用程序中,id
的值在myCollection
中是唯一的吗?当你说你重新分配myCollection
时,你是做myCollection = ...
还是像你的例子一样遍历列表并做myCollection[i] = ...
?
【参考方案1】:
我之前使用的一个快速技巧是将您的选择放在 ng-if 中,将 ng-if 设置为 false,然后在 $timeout 为 0 后将其设置回 true。这将导致 angular 重新渲染控件。
或者,您可以尝试使用 ng-repeat 自己呈现选项。不确定这是否可行。
【讨论】:
虽然这可能行得通,但听起来很肮脏,我不想这样做。 :)【参考方案2】:是的,它有点难看,需要一个难看的解决方法。
$timeout
解决方案通过给 AngularJS 一个更改来识别当前摘要循环中浅层属性已更改,如果您将该集合设置为[]
。
下次有机会通过$timeout
将其设置回原来的样子,AngularJS 会识别出浅层属性已更改为新的东西并相应地更新其ngOptions
。
我在演示中添加的另一件事是在更新集合之前存储当前选择的 ID。然后,当$timeout
代码恢复(更新的)集合时,它可以用于重新选择该选项。
演示: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);
;
]);
【讨论】:
这似乎是迄今为止建议的最合适的解决方案。仍然是一个丑陋的解决方法,我知道,这个机制会接受所有的变化,无论它们处于什么级别。基本上,这将重新渲染整个集合。 不过,我想知道,这对性能有什么影响吗?对集合的引用存储在临时变量中,稍后恢复。对我来说,这听起来像是一项低价手术。或者这是否会在后台触发 AngularJS 中任何昂贵的例程? 正如您所说,这是一个低价操作,因为 javascript 只传递指针的副本而不传递对象的副本。无论如何,Angular 都会进行浅层比较,所以据我所知,它的成本与两次更改第一级的成本相同......一点也不重要 我接受这个答案并将赏金奖励给你。两阶段更新效果很好,甚至选择的项目也能正常恢复。辛苦了,谢谢!【参考方案3】:changeFirstLevel
工作原理的解释
您正在使用(selectedOption.id + ' - ' + selectedOption.name)
表达式来呈现选择选项标签。这意味着 selectedOption.id + ' - ' + selectedOption.name
表达式适用于选择元素标签。当您调用changeFirstLevel
func 时,selectedOption
的name
将从name1
更改为newName1
。因为那个 html 是间接重新渲染的。
解决方案 1
如果性能对您来说不是问题,您只需删除track by
表达式即可解决问题。但是如果你想要同时进行性能和重新渲染,两者都会有点低。
解决方案 2
该指令正在深入观察变化并将其应用于model
。
var testApp = angular.module('testApp', []);
testApp.directive('collectionTracker', function()
return
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ngModel)
var oldCollection = [], newCollection = [], ngOptionCollection;
scope.$watch(
function() return ngModel.$modelValue ,
function(newValue, oldValue)
if( newValue != oldValue )
for( var i = 0; i < ngOptionCollection.length; i++ )
//console.log(i,newValue,ngOptionCollection[i]);
if( angular.equals(ngOptionCollection[i] , newValue ) )
newCollection = scope[attrs.collectionTracker];
setCollectionModel(i);
ngModel.$setUntouched();
break;
, true);
scope.$watch(attrs.collectionTracker, function( newValue, oldValue )
if( newValue != oldValue )
newCollection = newValue;
oldCollection = oldValue;
setCollectionModel();
, true)
scope.$watch(attrs.collectionTracker, function( newValue, oldValue )
if( newValue != oldValue || ngOptionCollection == undefined )
//console.log(newValue);
ngOptionCollection = angular.copy(newValue);
);
function setCollectionModel( index )
var oldIndex = -1;
if( index == undefined )
for( var i = 0; i < oldCollection.length; i++ )
if( angular.equals(oldCollection[i] , ngModel.$modelValue) )
oldIndex = i;
break;
else
oldIndex = index;
//console.log(oldIndex);
ngModel.$setViewValue(newCollection[oldIndex]);
);
testApp.controller('Ctrl', ['$scope', function ($scope)
$scope.myCollection = [
id: '1',
name: 'name1',
nested:
value: 'nested1'
,
id: '2',
name: 'name2',
nested:
value: 'nested2'
,
id: '3',
name: 'name3',
nested:
value: 'nested3'
];
$scope.changeFirstLevel = function()
var newElem =
id: '1',
name: 'name1',
nested:
value: 'newNested1'
;
$scope.myCollection[0] = newElem;
;
$scope.changeSecondLevel = function()
var newElem =
id: '1',
name: 'name1',
nested:
value: 'newNested2'
;
$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>
【讨论】:
【参考方案4】:您为什么不简单地通过该嵌套属性跟踪集合?
<select ng-model="selectedOption"
ng-options="(selectedOption.id + ' - ' + selectedOption.name) for selectedOption in myCollection track by selectedOption.nested.value">
更新
由于您不知道要跟踪哪个属性,您可以简单地通过表达式跟踪传递函数的所有属性。
ng-options="(selectedOption.id + ' - ' + selectedOption.name) for selectedOption in myCollection track by $scope.optionsTracker(selectedOption)"
在控制器上:
$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;
【讨论】:
有多个嵌套属性,我事先不知道,哪个属性会改变。 感谢您的努力,但我更喜欢更轻量级的解决方案,不太适合特定对象结构。【参考方案5】:我认为这里的任何解决方案都将是矫枉过正(新指令)或有点 hack($timeout)。
框架不会自动执行此操作是有原因的,我们已经知道这是性能。告诉 Angular 进行刷新通常会受到反对,imo。
所以,对我来说,我认为最不具侵入性的更改是添加一个 ng-change 方法并手动设置它,而不是依赖于 ng-model 更改。您仍然需要 ng-model ,但从现在开始它将是一个虚拟对象。您的集合将在 response 的返回 (.then ) 上分配,更不用说在那之后了。
所以,在控制器上:
$scope.change = function(obj)
$scope.selectedOption = obj;
并且每个按钮点击方法直接分配给对象:
$scope.selectedOption = newElem;
而不是
$scope.myCollection[0] = newElem;
正在观看:
<select ng-model="obj"
ng-options="(selectedOption.id + ' - ' + selectedOption.name) for selectedOption in myCollection track by selectedOption.id"
ng-change="change(obj)">
</select>
希望对你有帮助。
【讨论】:
你说告诉 Angular 刷新通常会被拒绝。你能详细说明一下吗? 自行管理更改对我来说不是一种选择,因为我的服务方法很可能会更改集合的多个元素,每个元素可能有也可能没有许多嵌套属性。 通常只有在处理 Angular 框架之外的 javascript 对象或来自不同库的 jquery 对象时才需要使用类似 $scope.$apply 的东西。使用 $timeout 之类的东西表明人们并不真正知道如何使用 $watches 或 $onChange 正确处理承诺或处理框架,因此我认为它不受欢迎。 如果没有看到您的架构,就很难争论,基于 jsfiddle 这将是我的一般解决方案。也许您可以将您的服务重构为返回一个新对象,而不是让它自己更改它?以上是关于在集合中的第二级更改后重新渲染 ng-options的主要内容,如果未能解决你的问题,请参考以下文章