Меня зовут Ислам, и недавно я присоединился к CSSSR. Так получилось, что теперь я изучаю и одновременно применяю AngularJS. Эта статья об одной из задач, вариантах её решения и граблях, на которые я наступил.
Warning: статья для начинающих и сочувствующих! Если код режет глаза, постарайтесь отнестись к нему с юмором. Все имена переменных и область применения вымышлены, любое совпадение с реальными случайно.
Задача
Для системы подачи заявок на выдачу корма зоопарка города N понадобилось реализовать новую фичу — выбор вольера и определенной клетки с помощью выпадающих списков. Исходные данные и требования:
- Бэкенд возвращает список всех вольеров;
- Каждый вольер содержит массив объектов-клеток;
- Для формирования заявки отправляется объект-заявка с необходимыми сведениями;
- Из первого списка выбирается вольер, в объект-заявку включается ID вольера;
- При выборе вольера во втором списке появляются опции, соответствующие выбранному вольеру;
- Из второго списка выбирается определенная клетка, в объект-заявку включается ID клетки;
- Существует вольер по умолчанию, у каждого вольера есть клетка по умолчанию.
Первое решение
Идея была проста: в первом списке показать все вольеры при помощи ng-repeat, в контроллере слить в один массив все клетки и (спасибо Stackoverflow) использовать фильтр для отображения нужных клеток во втором списке. Если нет ранее выбранных значений, то они выбираются по умолчанию.
<div class="form-group">
<select ng-model="request.volary_id" name="volary" class="form-control">
<option
ng-repeat="volary in volaries"
ng-selected="volary.volary_id == initVolary()"
value=""
></option>
</select>
</div>
<div class="form-group">
<select ng-model="request.cage_id" name="cage" class="form-control">
<option
ng-repeat="cage in cages | filter:{ volary_id : request.volary_id }"
ng-selected="cage.cage_id == initCage()"
value=""
></option>
</select>
</div>
$scope.cages = []
angular.forEach($scope.volaries, function (volary) {
angular.forEach(volary.cages, function (cage) {
if ((cage.volary_id = volary.volary_id)) {
$scope.cages.push(cage) //Собираем массив всех клеток
}
})
})
$scope.initVolary = function () {
//Если ранее вольер был выбран модель инициализируется этим значением, иначе - значением по умолчанию
var init //Для того, чтобы был выбран соответствующий option, возвращается значение init
if ($scope.request.volary_id) {
init = $scope.request.volary_id
} else {
angular.forEach($scope.volaries, function (volary) {
if (volary.is_default === 1) {
init = volary.volary_id
$scope.request.volary_id = volary.volary_id
}
})
}
console.log($scope.request.volary_id)
return init
}
$scope.initCage = function () {
//Если ранее была выбрана клетка модель инициализируется этим значением, иначе - значением по умолчанию
var init //Для того, чтобы был выбран соответствующий option, возвращается значение init
if ($scope.request.cage_id) {
init = $scope.request.cage_id
} else {
angular.forEach($scope.cages, function (cage) {
if (cage.is_default === 1 && cage.volary_id === $scope.request.volary_id) {
init = cage.cage_id
$scope.request.cage_id = cage.cage_id
}
})
}
return init
}
Второе решение
Первое решение работало не совсем корректно, и душа просила упростить реализацию. Один из старших товарищей предложил более простой вариант с ng-options
и применением логического приема, который помог отказаться от фильтра для клеток и кода из контроллера. Кроме того, было решено добавлять в request
объекты volary
и cage
целиком, а не только ID.
Подсказка
Часто мы забываем о том, какую большую роль может сыграть хороший логический прием.
<div class="form-group">
<select
ng-model="request.volary"
ng-options="volary as volary.text for volary in volaries"
class="form-control"
></select>
</div>
<div ng-if="request.volary" class="form-group">
// Отображать, если вольер уже выбран
<select
ng-model="request.cage"
ng-repeat="volary in volaries"
ng-show="request.volary == volary"
ng-options="cage as cage.text for cage in volary.cages"
class="form-control"
></select>
// Здесь мы проходим по всем вольерам и отображаем клетки из того вольера, который был выбран
ранее
</div>
Направление было задано, и мне нужно было поправить несколько вещей:
- Было непонятно, как установить значения по умолчанию;
- В модель по-прежнему записывался ID объекта, а не объект целиком;
- Если использовать
select cage
вместоselect cage.cage_id
записывался объект, но ломалось значениеvalue
в<option>
(Это было не очень важно, но не давало мне покоя); - У
<select>
вольеров и клеток есть уродливый пустой<option>
, от которого очень хотелось избавиться.
Подсказка
Не всегда есть возможность влиять на разные подсистемы проекта. Когда такая возможность есть, очень здорово посмотреть на решаемую задачу со всех сторон.
Первый пункт решили достаточно просто: на помощь пришел бэкенд. Если у request
не было ранее выбранных вольера и клетки, значения по умолчанию устанавливались на бэкенде. Немного позже это позволило отказаться и от повторяющихся select
для каждого вольера.
Подсказка
Читайте документацию и Stackoverflow внимательно. Невнимательность обязательно отнимет ваше время.
Чтение документации и советы со Stackoverflow помогли разобраться в тонкостях работы ng-options
. Выяснилось, что value
для ng-options
устанавливается при помощи track by
.
<div class="form-group">
<select
ng-model="request.volary"
ng-options="select volary.volary_id as volary.text for volary in volaries track by volary.volary_id"
class="form-control"
></select>
</div>
<div ng-if="request.volary" class="form-group">
<select
ng-model="request.cage"
ng-options="select cage.cage_id as cage.text for cage in volary.cages.data track by cage.cage_id"
class="form-control"
></select>
// track by указывает на значение, которое должно быть в value
</div>
Внимание!
Be careful when using select as
and track by
in the same expression. © angularjs.org
В предыдущем блоке ng-options
будет работать некорректно, подробнее читайте в документации по ng-options
Немного упорства в изучении тонкостей работы select as
и track by
, и пункты 2, 3 были решены.
<div class="form-group">
<select ng-model="request.volary" ng-options="volary.text for volary in volaries track by volary.volary_id" class="form-control"></select>
</div>
<div ng-if="request.volary" class="form-group">
<select ng-model="request.cage" ng-repeat="volary in volaries" ng-show="request.volary_id == volaries.volary_id" ng-options="cage.text for cage in volary.cages.data track by cage.cage_id" class="form-control">
</div>
Оказалось, что пустой <option>
возникает тогда, когда модель либо пуста, либо в ней находится значение, которого нет в коллекции. Этот интересный нюанс удалось освоить из-за того, что в request
начали записывать объекты.
Внимание!
Нужно быть осторожным с коллекциями, которые состоят из объектов. 1 === 1 всегда, но далеко не факт, что 2 объекта, у которых идентичны поля и их значения, будут равны. Помните, что объект — ссылочный тип данных.
Четвертый и заключительный пункт был уже частично решен: значение по умолчанию устанавливает бэкенд, пустого option
в select
вольера уже нет, и в модели всегда был объект вольера, который есть в перебираемой ng-options
коллекции. Лучшим решением показалось отображать во втором select
объекты-клетки, которые были включены в ранее выбранный объект-вольер.
Оставалось только проследить за тем, чтобы модели select
клетки всегда был объект из «правильной» коллекции. Для этого в контроллер была добавлена небольшая функция, которая вызывалась при выборе вольера.
$scope.setDefaultCage = function () {
angular.forEach($scope.request.volary.cages, function (cage) {
// Сейчас в request находится вольер, клетки которого нужно отобразить
if (cage.is_default) {
$scope.request.cage = cage // Перебираем все клетки этого вольера и записываем в модель клетку по умолчанию, это избавит от пустого option во втором select
}
})
}
Разметка после всех манипуляций и экспериментов:
<div class="form-group">
<select ng-model="request.volary" ng-change="setDefaultCage()" ng-options="volary.text for volary in volaries track by volary.volary_id" class="form-control"></select>
</div>
<div class="form-group">
<select ng-model="request.cage" ng-options="cage.text for cage in request.volary.cages track by cage.cage_id" class="form-control">
</div>
Конец
Надеюсь, что эта статья оказалась полезной и интересной. Смотрите работающие примеры кода из этой статьи в репозитории GitHub и ждите новых записок от начинающего Angular-онавта! :)
Ваш товарищ Ислам.