Wie reagiere ich auf Klicks auf ein Kontrollkästchen in einer AngularJS-Direktive?

79

Ich habe eine AngularJS- Direktive , die eine Sammlung von Entitäten in der folgenden Vorlage rendert:

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

Wie Sie sehen können, kann <table>hier jede Zeile einzeln mit einem eigenen Kontrollkästchen ausgewählt werden, oder es können alle Zeilen gleichzeitig mit einem Master-Kontrollkästchen im Feld ausgewählt werden <thead>. Ziemlich klassische Benutzeroberfläche.

Was ist der beste Weg, um:

  • Wählen Sie eine einzelne Zeile aus (dh wenn das Kontrollkästchen aktiviert ist, fügen Sie die ID der ausgewählten Entität einem internen Array hinzu und fügen Sie <tr>der Entität, die die Entität enthält , eine CSS-Klasse hinzu , um den ausgewählten Status wiederzugeben)?
  • Alle Zeilen gleichzeitig auswählen? (dh führen Sie die zuvor beschriebenen Aktionen für alle Zeilen in der <table>)

Meine aktuelle Implementierung besteht darin, meiner Direktive einen benutzerdefinierten Controller hinzuzufügen:

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

Genauer gesagt frage ich mich:

  • Gehört der obige Code in eine Steuerung oder sollte er in eine linkFunktion gehen?
  • Was ist der beste Weg, um DOM-Traversal durchzuführen, da jQuery nicht unbedingt vorhanden ist (AngularJS benötigt es nicht)? Ohne jQuery fällt es mir schwer, nur das übergeordnete <tr>Kontrollkästchen oder alle Kontrollkästchen in der Vorlage auszuwählen.
  • Die Weitergabe $eventan updateSelection()scheint nicht sehr elegant zu sein. Gibt es nicht eine bessere Möglichkeit, den Status (aktiviert / deaktiviert) eines Elements abzurufen, auf das gerade geklickt wurde?

Vielen Dank.

AngularChef
quelle

Antworten:

122

So habe ich solche Sachen gemacht. Angular tendiert eher zur deklarativen Manipulation des Doms als zu einer imperativen (zumindest habe ich so damit gespielt).

Das Markup

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

Und in der Steuerung

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

BEARBEITEN : getSelectedClass()Erwartet die gesamte Entität, wurde jedoch nur mit der ID der Entität aufgerufen, die jetzt korrigiert wird

Liviu T.
quelle
Danke, Liviu! Das funktioniert und es hat geholfen. Und dank Ihnen habe ich von der ngCheckedRichtlinie erfahren . (Ich bedaure nur, dass wir diesen Code nicht weniger ausführlich machen können.)
AngularChef
1
Betrachten Sie es nicht als ausführlich, sondern als Trennung von Bedenken. Ihre Datenmodelle sollten nicht wissen, wie sie dargestellt werden. Denken Sie daran, dass im Controller keine tr's oder td's erwähnt werden. höchstens enthält es das Kontrollkästchen, aber das könnte auch herausgerechnet werden. Sie können Ihren Controller jederzeit auf eine zweite Vorlage anwenden;)
Liviu T.
Danke für diese Frage und Antwort. Ich war neugierig auf die Auswirkungen dieses Ansatzes auf die Effizienz und habe diesen Plunkr erstellt: plnkr.co/edit/T5aZO3s5DzSnbrLELveG Ich habe festgestellt, dass isSelected jedes Mal, wenn ich eines der Elemente auswähle, 6 Mal aufgerufen wird (zweimal für jedes Repeater-Element). Irgendeine Idee, warum dies für jeden zweimal passiert? Hat jemand Bedenken, mehr als 100 Repeater-Elemente auf die Seite zu werfen und sie auf dem Handy auszuführen? Wahrscheinlich kein Problem ...
Aaronius
@Aaronius Wenn Sie einen Haltepunkt in die Funktion isSelected einfügen und aktualisieren, sehen Sie, dass dieser aufgerufen wird, bevor der Inhalt der Direktive analysiert und ausgeführt wird. Ich denke, da es eine Direktive ist, die alle gebundenen Funktionen ersetzt, werden sie zweimal aufgerufen
Liviu T.
Gibt es eine Methode, um nur die ausgewählten Kontrollkästchen zu kennen?
Sana Joseph
35

Ich bevorzuge die Anweisungen ngModel und ngChange, wenn es um Kontrollkästchen geht . Mit ngModel können Sie den aktivierten / deaktivierten Status des Kontrollkästchens an eine Eigenschaft der Entität binden:

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

Immer wenn der Benutzer das Kontrollkästchen aktiviert oder deaktiviert entity.isChecked, ändert sich auch der Wert.

Wenn dies alles ist, was Sie brauchen, brauchen Sie nicht einmal die Anweisungen ngClick oder ngChange. Da Sie das Kontrollkästchen "Alle überprüfen" haben, müssen Sie natürlich mehr tun, als nur den Wert der Eigenschaft festzulegen, wenn jemand ein Kontrollkästchen aktiviert.

Wenn Sie ngModel mit einem Kontrollkästchen verwenden, verwenden Sie am besten ngChange anstelle von ngClick, um aktivierte und nicht aktivierte Ereignisse zu behandeln. ngChange ist genau für diese Art von Szenario gemacht. Der ngModelController wird für die Datenbindung verwendet (er fügt dem $viewChangeListenersArray des ngModelController einen Listener hinzu . Die Listener in diesem Array werden aufgerufen, nachdem der Modellwert festgelegt wurde, um dieses Problem zu vermeiden ).

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

... und in der Steuerung ...

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

Ebenso das Kontrollkästchen "Alle prüfen" in der Kopfzeile:

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

... und ...

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

Was ist der beste Weg, um ... eine CSS-Klasse zu <tr>der Entität hinzuzufügen , die den ausgewählten Status widerspiegelt?

Wenn Sie den ngModel-Ansatz für die Datenbindung verwenden, müssen Sie dem Element lediglich die ngClass- Direktive <tr>hinzufügen, um die Klasse dynamisch hinzuzufügen oder zu entfernen, wenn sich die Entitätseigenschaft ändert:

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

Den vollständigen Plunker finden Sie hier .

Kevin Aenmey
quelle
Das Flag allItemsSelected wird am Anfang auf false gesetzt und dann auf true gesetzt, wenn Sie auf das Kontrollkästchen Alle auswählen klicken. Kannst du bitte Erklären?
user2514925
11

Livius Antwort war sehr hilfreich für mich. Hoffe, das ist keine schlechte Form, aber ich habe eine Geige gemacht , die in Zukunft jemand anderem helfen könnte.

Zwei wichtige Teile, die benötigt werden, sind:

    $scope.entities = [{
    "title": "foo",
    "id": 1
}, {
    "title": "bar",
    "id": 2
}, {
    "title": "baz",
    "id": 3
}];
$scope.selected = [];
VBAHole
quelle
1
Angular Docs haben eine einfachere Antwort für den Check All-Teil. docs.angularjs.org/api/ng.directive:ngChecked . Ich versuche herauszufinden, was überprüft wird.
Hayden