Jak reagować na kliknięcia pola wyboru w dyrektywie AngularJS?

79

Mam dyrektywę AngularJS, która renderuje kolekcję jednostek w następującym szablonie:

<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>

Jak widać, jest to miejsce, w <table>którym każdy wiersz można wybrać indywidualnie za pomocą własnego pola wyboru lub wszystkie wiersze można zaznaczyć jednocześnie za pomocą głównego pola wyboru znajdującego się w <thead>. Całkiem klasyczny interfejs użytkownika.

Jaki jest najlepszy sposób, aby:

  • Wybierz pojedynczy wiersz (tj. Gdy pole wyboru jest zaznaczone, dodaj identyfikator wybranej jednostki do wewnętrznej tablicy i dodaj klasę CSS do jednostki <tr>zawierającej jednostkę, aby odzwierciedlić jej wybrany stan)?
  • Wybrać wszystkie wiersze jednocześnie? (tj. wykonaj opisane wcześniej czynności dla wszystkich wierszy w <table>)

Moja obecna implementacja polega na dodaniu niestandardowego kontrolera do mojej dyrektywy:

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??
    };
}

Dokładniej, zastanawiam się:

  • Czy powyższy kod należy do kontrolera, czy powinien znajdować się w linkfunkcji?
  • Biorąc pod uwagę, że jQuery niekoniecznie jest obecny (AngularJS tego nie wymaga), jaki jest najlepszy sposób na przechodzenie przez DOM? Bez jQuery trudno mi po prostu zaznaczyć rodzica <tr>danego pola wyboru lub zaznaczyć wszystkie pola wyboru w szablonie.
  • Przejście $eventdo updateSelection()nie wydaje się zbyt eleganckie. Czy nie ma lepszego sposobu na pobranie stanu (zaznaczonego / niezaznaczonego) elementu, który właśnie został kliknięty?

Dziękuję Ci.

AngularChef
źródło

Odpowiedzi:

122

W ten sposób robię tego typu rzeczy. Angular preferuje raczej deklaratywną manipulację domem niż imperatywną (przynajmniej tak się bawiłem).

Znaczniki

<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>

I w kontrolerze

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;
};

EDYCJA : getSelectedClass()oczekuje całej jednostki, ale była wywoływana tylko z identyfikatorem jednostki, który jest teraz poprawiany

Liviu T.
źródło
Dzięki, Liviu! To działa i pomogło. Dzięki tobie dowiedziałem się o ngCheckeddyrektywie. (Żałuję tylko, że nie możemy uczynić tego kodu nieco mniej szczegółowym.)
AngularChef
1
Nie myśl o tym jako o szczegółach, myśl o tym w kategoriach oddzielenia obaw. Twoje modele danych nie powinny wiedzieć, w jaki sposób są prezentowane. Pamiętaj, że w kontrolerze nie ma wzmianki o tr lub td. co najwyżej zawiera pole wyboru, ale można to również uwzględnić. Zawsze możesz wziąć swój kontroler i zastosować go do drugiego szablonu;)
Liviu T.
Dzięki za to pytanie i odpowiedź. Byłem ciekawy konsekwencji tego podejścia dla wydajności, więc stworzyłem ten plunkr: plnkr.co/edit/T5aZO3s5DzSnbrLELveG Zauważyłem, że za każdym razem, gdy wybieram jeden z elementów, isSelected jest wywoływane 6 razy (dwa razy dla każdego elementu wzmacniacza). Masz jakiś pomysł, dlaczego dzieje się to dwa razy dla każdego? Czy ktoś obawia się wrzucenia ponad 100 elementów repeatera na stronę i uruchomienia jej na telefonie komórkowym? Raczej nie będzie problemu ...
Aaronius
@Aaronius Jeśli dodasz punkt przerwania w funkcji isSelected i odświeżysz, zobaczysz, że jest on wywoływany, zanim zawartość dyrektywy zostanie przeanalizowana i wykonana. Myślę, że ponieważ jest to dyrektywa, która zastępuje wszystkie związane funkcje, są wywoływane dwukrotnie
Liviu T.
Czy istnieje metoda, aby poznać tylko wybrane pola wyboru?
Sana Joseph
35

Wolę używać dyrektyw ngModel i ngChange w przypadku pól wyboru . ngModel umożliwia powiązanie zaznaczonego / niezaznaczonego stanu pola wyboru z właściwością jednostki:

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

Za każdym razem, gdy użytkownik zaznaczy lub usunie zaznaczenie pola wyboru, entity.isCheckedwartość również się zmieni.

Jeśli to wszystko, czego potrzebujesz, nie potrzebujesz nawet dyrektyw ngClick ani ngChange. Ponieważ masz pole wyboru „Zaznacz wszystko”, musisz oczywiście zrobić coś więcej niż tylko ustawić wartość właściwości, gdy ktoś zaznaczy pole wyboru.

Korzystając z ngModel z polem wyboru, najlepiej jest używać ngChange zamiast ngClick do obsługi zaznaczonych i niezaznaczonych zdarzeń. ngChange jest stworzony właśnie dla tego rodzaju scenariusza. Wykorzystuje ngModelController do wiązania danych (dodaje detektor do $viewChangeListenerstablicy ngModelControllera . Słuchacze w tej tablicy są wywoływani po ustawieniu wartości modelu, co pozwala uniknąć tego problemu ).

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

... iw kontrolerze ...

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;
};

Podobnie pole wyboru „Zaznacz wszystko” w nagłówku:

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

... i ...

// 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

Jaki jest najlepszy sposób ... dodania klasy CSS do elementu <tr>zawierającego encję, aby odzwierciedlić jej wybrany stan?

Jeśli używasz podejścia ngModel do wiązania danych, wszystko, co musisz zrobić, to dodać dyrektywę ngClass do <tr>elementu, aby dynamicznie dodawać lub usuwać klasę za każdym razem, gdy zmienia się właściwość jednostki:

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

Zobacz pełną wersję Plunkera tutaj .

Kevin Aenmey
źródło
Flaga allItemsSelected jest ustawiona na false na początku, a następnie na wartość true po kliknięciu pola wyboru zaznacz wszystko. czy możesz wyjaśnić?
user2514925
11

Odpowiedź Liviu była dla mnie niezwykle pomocna. Mam nadzieję, że to nie jest zła forma, ale zrobiłem skrzypce, które mogą pomóc komuś w przyszłości.

Dwa ważne elementy, które są potrzebne, to:

    $scope.entities = [{
    "title": "foo",
    "id": 1
}, {
    "title": "bar",
    "id": 2
}, {
    "title": "baz",
    "id": 3
}];
$scope.selected = [];
VBAHole
źródło
1
Angular Docs mają prostszą odpowiedź na sprawdzenie wszystkich części. docs.angularjs.org/api/ng.directive:ngChecked . Zbieranie tego, co jest sprawdzane, to coś, co próbuję zrozumieć.
Hayden