web-dev-qa-db-fra.com

Comment répondre aux clics sur une case à cocher d'une directive AngularJS?

J'ai un AngularJS directive qui rend une collection d'entités dans le modèle suivant:

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

Comme vous pouvez le constater, il s’agit d’un <table> dans lequel chaque ligne peut être sélectionnée individuellement avec sa propre case à cocher, ou toutes les lignes peuvent être sélectionnées en même temps avec une case à cocher principale située dans le <thead>. Jolie interface classique.

Quel est le meilleur moyen de:

  • Sélectionnez une seule ligne (c'est-à-dire, lorsque la case à cocher est cochée, ajoutez l'ID de l'entité sélectionnée à un tableau interne et ajoutez une classe CSS à <tr> contenant l'entité pour refléter son état sélectionné)?
  • Sélectionner toutes les lignes à la fois? (c’est-à-dire effectuer les actions décrites précédemment pour toutes les lignes du <table>)

Mon implémentation actuelle consiste à ajouter un contrôleur personnalisé à ma directive:

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

Plus précisément, je me demande:

  • Le code ci-dessus appartient-il à un contrôleur ou doit-il aller à une fonction link?
  • Etant donné que jQuery n’est pas nécessairement présent (AngularJS ne l’exige pas), quel est le meilleur moyen de faire une traversée DOM? Sans jQuery, je ne parviens pas à sélectionner le parent <tr> d'une case à cocher donnée ou à sélectionner toutes les cases à cocher du modèle.
  • Passer $event à updateSelection() ne semble pas très élégant. N'y a-t-il pas un meilleur moyen de récupérer l'état (coché/décoché) d'un élément qui vient d'être cliqué?

Je vous remercie.

79
AngularChef

C'est comme ça que je fais ce genre de choses. Angular a tendance à favoriser la manipulation déclarative du dom plutôt qu’une manipulation impérative (du moins c’est comme ça que j’ai joué avec elle).

Le balisage

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

Et dans le contrôleur

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

EDIT: getSelectedClass() attend l'entité entière mais elle était appelée avec l'identifiant de l'entité uniquement, ce qui est maintenant corrigé

122
Liviu T.

Je préfère utiliser le ngModel et ngChange directives quand traitant des cases à cocher . ngModel vous permet de lier l'état activé/désactivé de la case à cocher à une propriété de l'entité:

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

Chaque fois que l'utilisateur coche ou décoche la case, la valeur entity.isChecked change également.

Si c'est tout ce dont vous avez besoin, vous n'avez même pas besoin des directives ngClick ou ngChange. Puisque vous avez la case à cocher "Tout cocher", vous devez évidemment faire plus que simplement définir la valeur de la propriété lorsque quelqu'un coche une case à cocher.

Lorsque vous utilisez ngModel avec une case à cocher, il est préférable d’utiliser ngChange plutôt que ngClick pour gérer les événements cochés et non cochés. ngChange est fait pour ce genre de scénario. Il utilise le ngModelController pour la liaison de données (il ajoute un écouteur au tableau $viewChangeListeners du ngModelController. Les écouteurs de ce tableau sont appelés après la valeur du modèle a été définie, en évitant ce problème ).

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

... et dans le contrôleur ...

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

De même, la case à cocher "Tout vérifier" dans l'en-tête:

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

... et ...

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

Quel est le meilleur moyen de ... ajouter une classe CSS au <tr> contenant l'entité pour refléter l'état sélectionné?

Si vous utilisez l'approche ngModel pour la liaison de données, il vous suffit d'ajouter la directive ngClass à l'élément <tr> pour ajouter ou supprimer de manière dynamique la classe chaque fois que la propriété de l'entité change:

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

Voir le complet Plunker here .

35
Kevin Aenmey

La réponse de Liviu m'a été extrêmement utile. J'espère que ce n'est pas une mauvaise forme, mais j'ai fait un violon qui pourrait aider quelqu'un d'autre à l'avenir.

Deux pièces importantes nécessaires sont:

    $scope.entities = [{
    "title": "foo",
    "id": 1
}, {
    "title": "bar",
    "id": 2
}, {
    "title": "baz",
    "id": 3
}];
$scope.selected = [];
11
VBAHole