Как реагировать на нажатие флажка в директиве AngularJS?

79

У меня есть директива AngularJS, которая отображает коллекцию сущностей в следующем шаблоне:

<table class="table">
  <thead>
    <tr>
      <th><input type="checkbox" ng-click="selectAll()"></th>
      <th>Title</th>
    </tr>
  </thead>
  <tbody>
    <tr ng-repeat="e in entities">
      <td><input type="checkbox" name="selected" ng-click="updateSelection($event, e.id)"></td>
      <td>{{e.title}}</td>
    </tr>
  </tbody>
</table>

Как видите, здесь <table>каждая строка может быть выбрана индивидуально с ее собственным флажком, или все строки могут быть выбраны сразу с помощью основного флажка, расположенного в <thead>. Довольно классический интерфейс.

Как лучше всего:

  • Выбрать одну строку (т.е. когда флажок установлен, добавить идентификатор выбранной сущности во внутренний массив и добавить класс CSS к <tr>содержащему сущность, чтобы отразить ее выбранное состояние)?
  • Выбрать сразу все строки? (т.е. выполните ранее описанные действия для всех строк в <table>)

Моя текущая реализация - добавить настраиваемый контроллер в мою директиву:

controller: function($scope) {

    // Array of currently selected IDs.
    var selected = $scope.selected = [];

    // Update the selection when a checkbox is clicked.
    $scope.updateSelection = function($event, id) {

        var checkbox = $event.target;
        var action = (checkbox.checked ? 'add' : 'remove');
        if (action == 'add' & selected.indexOf(id) == -1) selected.push(id);
        if (action == 'remove' && selected.indexOf(id) != -1) selected.splice(selected.indexOf(id), 1);

        // Highlight selected row. HOW??
        // $(checkbox).parents('tr').addClass('selected_row', checkbox.checked);
    };

    // Check (or uncheck) all checkboxes.
    $scope.selectAll = function() {
        // Iterate on all checkboxes and call updateSelection() on them??
    };
}

В частности, мне интересно:

  • Принадлежит ли приведенный выше код контроллеру или он должен входить в linkфункцию?
  • Учитывая, что jQuery не обязательно присутствует (AngularJS не требует этого), как лучше всего выполнять обход DOM? Без jQuery мне сложно просто выбрать родительский <tr>элемент для данного флажка или выбрать все флажки в шаблоне.
  • Переход $eventк updateSelection()не выглядит изящным. Нет ли лучшего способа получить состояние (отмечен / не отмечен) элемента, по которому только что щелкнули?

Спасибо.

AngularChef
источник

Ответы:

122

Вот так я и делал подобные вещи. Angular имеет тенденцию отдавать предпочтение декларативным манипуляциям с dom, а не императивным (по крайней мере, так я играл с этим).

Разметка

<table class="table">
  <thead>
    <tr>
      <th>
        <input type="checkbox" 
          ng-click="selectAll($event)"
          ng-checked="isSelectedAll()">
      </th>
      <th>Title</th>
    </tr>
  </thead>
  <tbody>
    <tr ng-repeat="e in entities" ng-class="getSelectedClass(e)">
      <td>
        <input type="checkbox" name="selected"
          ng-checked="isSelected(e.id)"
          ng-click="updateSelection($event, e.id)">
      </td>
      <td>{{e.title}}</td>
    </tr>
  </tbody>
</table>

И в контроллере

var updateSelected = function(action, id) {
  if (action === 'add' && $scope.selected.indexOf(id) === -1) {
    $scope.selected.push(id);
  }
  if (action === 'remove' && $scope.selected.indexOf(id) !== -1) {
    $scope.selected.splice($scope.selected.indexOf(id), 1);
  }
};

$scope.updateSelection = function($event, id) {
  var checkbox = $event.target;
  var action = (checkbox.checked ? 'add' : 'remove');
  updateSelected(action, id);
};

$scope.selectAll = function($event) {
  var checkbox = $event.target;
  var action = (checkbox.checked ? 'add' : 'remove');
  for ( var i = 0; i < $scope.entities.length; i++) {
    var entity = $scope.entities[i];
    updateSelected(action, entity.id);
  }
};

$scope.getSelectedClass = function(entity) {
  return $scope.isSelected(entity.id) ? 'selected' : '';
};

$scope.isSelected = function(id) {
  return $scope.selected.indexOf(id) >= 0;
};

//something extra I couldn't resist adding :)
$scope.isSelectedAll = function() {
  return $scope.selected.length === $scope.entities.length;
};

РЕДАКТИРОВАТЬ : getSelectedClass()ожидает весь объект, но он был вызван только с идентификатором объекта, который теперь исправлен

Ливиу Т.
источник
Спасибо, Ливиу! Это работает, и это помогло. И благодаря вам я узнал о ngCheckedдирективе. (Я сожалею только о том, что мы не можем сделать этот код менее подробным.)
AngularChef,
1
Не думайте об этом как о многословном, подумайте об этом с точки зрения разделения проблем. Ваши модели данных не должны знать, как они представлены. Помните, что в контроллере нет упоминаний о tr или td. в лучшем случае он содержит флажок, но его также можно исключить. Вы всегда можете взять свой контроллер и применить его ко второму шаблону;)
Ливиу Т.
Спасибо за этот вопрос и ответ. Мне было любопытно узнать о последствиях этого подхода для эффективности, поэтому я сделал этот plunkr: plnkr.co/edit/T5aZO3s5DzSnbrLELveG Я заметил, что каждый раз, когда я выбираю один из элементов, isSelected вызывается 6 раз (дважды для каждого элемента репитера). Есть идеи, почему это происходит дважды для каждого? Кого-нибудь беспокоит размещение 100+ репитеров на странице и запуск их на мобильном устройстве? Наверное, не будет проблемой ...
Аарониус
@Aaronius. Если вы добавите точку останова в функцию isSelected и обновите ее, вы увидите, что она вызывается до того, как содержимое директивы будет проанализировано и выполнено. Я думаю, поскольку это директива, которая выполняет замену, все связанные функции вызываются дважды
Ливиу Т.
Есть ли способ узнать только выбранные флажки?
Сана Джозеф
35

Я предпочитаю использовать директивы ngModel и ngChange при работе с флажками . ngModel позволяет вам привязать отмеченное / не отмеченное состояние флажка к свойству объекта:

<input type="checkbox" ng-model="entity.isChecked">

Каждый раз, когда пользователь устанавливает или снимает флажок, entity.isCheckedзначение также изменяется.

Если это все, что вам нужно, вам даже не нужны директивы ngClick или ngChange. Поскольку у вас есть флажок «Проверить все», очевидно, что вам нужно сделать больше, чем просто установить значение свойства, когда кто-то проверяет флажок.

При использовании ngModel с флажком лучше использовать ngChange, а не ngClick для обработки отмеченных и непроверенных событий. ngChange создан именно для такого сценария. Он использует ngModelController для привязки данных (он добавляет слушателя к массиву ngModelController $viewChangeListeners. Слушатели в этом массиве вызываются после установки значения модели, что позволяет избежать этой проблемы ).

<input type="checkbox" ng-model="entity.isChecked" ng-change="selectEntity()">

... а в контроллере ...

var model = {};
$scope.model = model;

// This property is bound to the checkbox in the table header
model.allItemsSelected = false;

// Fired when an entity in the table is checked
$scope.selectEntity = function () {
    // If any entity is not checked, then uncheck the "allItemsSelected" checkbox
    for (var i = 0; i < model.entities.length; i++) {
        if (!model.entities[i].isChecked) {
            model.allItemsSelected = false;
            return;
        }
    }

    // ... otherwise ensure that the "allItemsSelected" checkbox is checked
    model.allItemsSelected = true;
};

Аналогично галочка «Проверить все» в шапке:

<th>
    <input type="checkbox" ng-model="model.allItemsSelected" ng-change="selectAll()">
</th>

... и ...

// Fired when the checkbox in the table header is checked
$scope.selectAll = function () {
    // Loop through all the entities and set their isChecked property
    for (var i = 0; i < model.entities.length; i++) {
        model.entities[i].isChecked = model.allItemsSelected;
    }
};

CSS

Как лучше всего ... добавить класс CSS к <tr>содержащей сущности, чтобы отразить его выбранное состояние?

Если вы используете подход ngModel для привязки данных, все, что вам нужно сделать, это добавить директиву ngClass к <tr>элементу для динамического добавления или удаления класса при изменении свойства объекта:

<tr ng-repeat="entity in model.entities" ng-class="{selected: entity.isChecked}">

См. Полный Plunker здесь .

Кевин Эйнми
источник
Для флага allItemsSelected вначале устанавливается значение false, а затем устанавливается значение true при установке флажка «Выбрать все». не могли бы вы объяснить?
user2514925
11

Ответ Ливиу был мне чрезвычайно полезен. Надеюсь, это не дурной тон, но я сделал скрипку, которая может помочь кому-то другому в будущем.

Необходимы две важные составляющие:

    $scope.entities = [{
    "title": "foo",
    "id": 1
}, {
    "title": "bar",
    "id": 2
}, {
    "title": "baz",
    "id": 3
}];
$scope.selected = [];
VBAHole
источник
1
В документах Angular есть более простой ответ для проверки всей части. docs.angularjs.org/api/ng.directive:ngChecked . Я пытаюсь понять, как собирать то, что проверено.
Hayden