¿Cómo responder a los clics en una casilla de verificación en una directiva AngularJS?

79

Tengo una directiva AngularJS que representa una colección de entidades en la siguiente plantilla:

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

Como puede ver, es un lugar <table>donde cada fila se puede seleccionar individualmente con su propia casilla de verificación, o todas las filas se pueden seleccionar a la vez con una casilla de verificación maestra ubicada en el <thead>. Interfaz de usuario bastante clásica.

Cuál es la mejor forma de:

  • Seleccione una sola fila (es decir, cuando la casilla de verificación esté marcada, agregue la identificación de la entidad seleccionada a una matriz interna y agregue una clase CSS al que <tr>contiene la entidad para reflejar su estado seleccionado).
  • ¿Seleccionar todas las filas a la vez? (es decir, realice las acciones descritas anteriormente para todas las filas del<table> )

Mi implementación actual es agregar un controlador personalizado a mi directiva:

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

Más específicamente, me pregunto:

  • ¿El código anterior pertenece a un controlador o debería ir en un link función?
  • Dado que jQuery no está necesariamente presente (AngularJS no lo requiere), ¿cuál es la mejor manera de hacer un recorrido DOM? Sin jQuery, estoy teniendo dificultades para seleccionar el padre <tr>de una casilla de verificación determinada o seleccionar todas las casillas de verificación en la plantilla.
  • Pasar $eventa updateSelection()no parece muy elegante. ¿No hay una mejor manera de recuperar el estado (marcado / desmarcado) de un elemento en el que se acaba de hacer clic?

Gracias.

AngularChef
fuente

Respuestas:

122

Así es como he estado haciendo este tipo de cosas. Angular tiende a favorecer la manipulación declarativa del dom en lugar de una imperativa (al menos así es como he estado jugando con él).

El marcado

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

Y en el controlador

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

EDITAR : getSelectedClass()espera toda la entidad, pero se estaba llamando solo con la identificación de la entidad, que ahora está corregida

Liviu T.
fuente
¡Gracias Liviu! Eso funciona y ayudó. Y gracias a ti, me enteré de la ngCheckeddirectiva. (Lo único que lamento es que no podamos hacer que este código sea un poco menos detallado.)
AngularChef
1
No lo considere detallado, piénselo en términos de separación de preocupaciones. Sus modelos de datos no deben conocer la forma en que se presentan. Recuerde que en el controlador no se mencionan tr ni td. a lo sumo contiene la casilla de verificación, pero eso también podría descartarse. Siempre puede tomar su controlador y aplicarlo a una segunda plantilla;)
Liviu T.
Gracias por esta pregunta y respuesta. Tenía curiosidad por conocer las implicaciones de eficiencia de este enfoque, así que hice este plunkr: plunkr: plunkr . ¿Alguna idea de por qué sucede esto dos veces para cada uno? ¿Alguien le preocupa lanzar más de 100 elementos repetidores en la página y ejecutarlo en un dispositivo móvil? Probablemente no sea un problema ...
Aaronius
@Aaronius Si agrega un punto de interrupción en la función isSelected y actualiza, verá que se llama antes de que se analice y ejecute el contenido de la directiva. Creo que, dado que es una directiva que reemplaza, todas las funciones vinculadas se llaman dos veces
Liviu T.
¿Existe algún método para conocer únicamente las casillas de verificación seleccionadas?
Sana Joseph
35

Prefiero usar las directivas ngModel y ngChange cuando trato con casillas de verificación . ngModel le permite vincular el estado marcado / no marcado de la casilla de verificación a una propiedad de la entidad:

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

Siempre que el usuario marque o desmarque la casilla de verificación, el entity.isCheckedvalor también cambiará.

Si esto es todo lo que necesita, entonces ni siquiera necesita las directivas ngClick o ngChange. Dado que tiene la casilla de verificación "Marcar todo", obviamente necesita hacer más que simplemente establecer el valor de la propiedad cuando alguien marca una casilla de verificación.

Al usar ngModel con una casilla de verificación, es mejor usar ngChange en lugar de ngClick para manejar eventos marcados y no marcados. ngChange está diseñado para este tipo de escenario. Hace uso de ngModelController para el enlace de datos (agrega un oyente a la matriz de ngModelController $viewChangeListeners. Los oyentes en esta matriz son llamados después de que se haya establecido el valor del modelo, evitando este problema ).

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

... y en el controlador ...

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

Del mismo modo, la casilla de verificación "Marcar todo" en el encabezado:

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

... y ...

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

¿Cuál es la mejor manera de ... agregar una clase CSS al que <tr>contiene la entidad para reflejar su estado seleccionado?

Si usa el enfoque ngModel para el enlace de datos, todo lo que necesita hacer es agregar la directiva ngClass al <tr>elemento para agregar o eliminar dinámicamente la clase siempre que cambie la propiedad de la entidad:

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

Vea el Plunker completo aquí .

Kevin Aenmey
fuente
El indicador allItemsSelected se establece en falso al principio y luego cómo se establece en verdadero cuando se hace clic en la casilla de verificación Seleccionar todo. ¿Puede usted explicar por favor?
user2514925
11

La respuesta de Liviu fue de gran ayuda para mí. Espero que esto no sea una mala forma, pero hice un violín que puede ayudar a alguien más en el futuro.

Dos piezas importantes que se necesitan son:

    $scope.entities = [{
    "title": "foo",
    "id": 1
}, {
    "title": "bar",
    "id": 2
}, {
    "title": "baz",
    "id": 3
}];
$scope.selected = [];
VBAHole
fuente
1
Los documentos angulares tienen una respuesta más simple para la parte de verificación completa. docs.angularjs.org/api/ng.directive:ngChecked . Recopilar lo que se marca es algo que estoy tratando de averiguar.
Hayden