Cómo hacer que ng-repeat filtre resultados duplicados

131

Estoy ejecutando un simple ng-repeatsobre un archivo JSON y quiero obtener nombres de categoría. Hay alrededor de 100 objetos, cada uno perteneciente a una categoría, pero solo hay alrededor de 6 categorías.

Mi código actual es este:

<select ng-model="orderProp" >
  <option ng-repeat="place in places" value="{{place.category}}">{{place.category}}</option>
</select>

La salida es de 100 opciones diferentes, en su mayoría duplicadas. ¿Cómo uso Angular para verificar si {{place.category}}ya existe un y no crear una opción si ya está allí?

editar: en mi javascript $scope.places = JSON data, solo para aclarar

JVG
fuente
1
¿Por qué no solo deduce sus $ scope.places? use jquery map api.jquery.com/map
anazimok
¿Cuál fue su solución de trabajo final? ES que está arriba o hay algún JS haciendo el
desduplicado
Quiero saber la solución a esto. Por favor, publique un seguimiento. ¡Gracias!
jdstein1
@ jdstein1 Comenzaré con un TLDR: use las respuestas a continuación, o use Javascript vainilla para filtrar solo valores únicos en una matriz. Lo que hice: Al final, fue un problema con mi lógica y mi comprensión de MVC. Estaba cargando datos de MongoDB pidiendo un volcado de datos y quería que Angular los filtrara mágicamente a lugares únicos. La solución, como suele ser el caso, era dejar de ser flojo y arreglar mi modelo de base de datos; para mí, era llamar a Mongo db.collection.distinct("places"), que era mucho, mucho mejor que hacerlo en Angular. Lamentablemente, esto no funcionará para todos.
JVG
¡gracias por la actualización!
jdstein1

Respuestas:

142

Puede usar el filtro único de AngularUI (código fuente disponible aquí: filtro único de AngularUI ) y usarlo directamente en las opciones ng (o ng-repeat).

<select ng-model="orderProp" ng-options="place.category for place in places | unique:'category'">
    <option value="0">Default</option>
    // unique options from the categories
</select>
jpmorin
fuente
33
Para aquellos que no pueden hacer funcionar el filtro único de AngularUI: los filtros están en un módulo separado: por ejemplo, debe incluirlo como referencia adicional en su módulo angular.module('yourModule', ['ui', 'ui.filters']);. Estaba perplejo hasta que eché un vistazo dentro del archivo AngularUI js.
GFoley83
8
El uniquefiltro se puede encontrar actualmente como parte de AngularJs UI Utils
Nick
2
En la nueva versión de ui utils, puede incluir ui.unique por sí solo. use bower install solo para el módulo.
Estadísticas de aprendizaje con el ejemplo del
Si no desea incluir AngularUI y su totalidad, pero desea utilizar el filtro único, puede copiar y pegar la fuente unique.js en su aplicación, luego cambiar angular.module('ui.filters')el nombre de su aplicación.
chakeda
37

O puede escribir su propio filtro usando lodash.

app.filter('unique', function() {
    return function (arr, field) {
        return _.uniq(arr, function(a) { return a[field]; });
    };
});
Mike Ward
fuente
Hola Mike, se ve muy elegante pero no parece funcionar como se esperaba cuando paso un filtro como único: estado [mi nombre de campo] solo devuelve el primer resultado en oposición a la lista deseada de todas las estatuas únicas disponibles. ¿Alguna idea de por qué?
SinSync
3
Aunque usé guión bajo, esta solución funcionó de maravilla. ¡Gracias!
Jon Black
Solución muy elegante.
JD Smith
1
Lodash es un tenedor de guiones bajos. Tiene mejor rendimiento y más utilidades.
Mike Ward,
2
en lodash v4 _.uniqBy(arr, field);debería funcionar para propiedades anidadas
ijavid
30

Puede usar el filtro 'único' (alias: uniq) en el módulo angular.filter

uso: colection | uniq: 'property'
también puede filtrar por propiedades anidadas: colection | uniq: 'property.nested_property'

Lo que puedes hacer es algo así ...

function MainController ($scope) {
 $scope.orders = [
  { id:1, customer: { name: 'foo', id: 10 } },
  { id:2, customer: { name: 'bar', id: 20 } },
  { id:3, customer: { name: 'foo', id: 10 } },
  { id:4, customer: { name: 'bar', id: 20 } },
  { id:5, customer: { name: 'baz', id: 30 } },
 ];
}

HTML: filtramos por ID de cliente, es decir, eliminamos clientes duplicados

<th>Customer list: </th>
<tr ng-repeat="order in orders | unique: 'customer.id'" >
   <td> {{ order.customer.name }} , {{ order.customer.id }} </td>
</tr>

resultado
Lista de clientes:
foo 10
bar 20
baz 30

a8m
fuente
como su respuesta, pero me resulta difícil traducirla a mi problema. ¿Cómo manejarías una lista anidada? Por ejemplo, una lista de pedidos que contenía una lista de artículos para cada pedido. En su ejemplo, tiene un cliente por pedido. Me gustaría ver una lista única de artículos en todos los pedidos. No es para diferir el punto, pero también puede pensar que un cliente tiene muchos pedidos y un pedido tiene muchos artículos. ¿Cómo puedo mostrar todos los artículos anteriores que ha pedido un cliente? Pensamientos?
Greg Grater
@GregGrater ¿Puede proporcionar un objeto de ejemplo similar a su problema?
a8m
Gracias @Ariel M.! Aquí hay un ejemplo de los datos:
Greg Grater
¿Con qué versiones de angular es compatible?
Icet
15

Este código me funciona.

app.filter('unique', function() {

  return function (arr, field) {
    var o = {}, i, l = arr.length, r = [];
    for(i=0; i<l;i+=1) {
      o[arr[i][field]] = arr[i];
    }
    for(i in o) {
      r.push(o[i]);
    }
    return r;
  };
})

y entonces

var colors=$filter('unique')(items,"color");
Eduardo Ortiz
fuente
2
La definición de l debería ser l = arr! = Undefined? arr.length: 0 ya que de lo contrario hay un error de análisis en angularjs
Gerrit
6

Si desea enumerar categorías, creo que debe indicar explícitamente su intención en la vista.

<select ng-model="orderProp" >
  <option ng-repeat="category in categories"
          value="{{category}}">
    {{category}}
  </option>
</select>

en el controlador:

$scope.categories = $scope.places.reduce(function(sum, place) {
  if (sum.indexOf( place.category ) < 0) sum.push( place.category );
  return sum;
}, []);
Tosh
fuente
¿Podría explicar esto un poco más? Quiero repetir los elementos seguidos para cada categoría única. ¿El JS solo va en el archivo JS donde se define el controlador?
mark1234
Si desea agrupar las opciones, es una pregunta diferente. Es posible que desee buscar en el elemento optgroup. Soportes angulares optgroup. busca la group byexpresión en la selectdirectiva.
Tosh
¿Sucederá en caso de que se agregue / elimine un lugar? ¿Se agregará / eliminará la categoría asociada si es la única instancia en lugares?
Greg Grater
4

Aquí hay un ejemplo sencillo y genérico.

El filtro:

sampleApp.filter('unique', function() {

  // Take in the collection and which field
  //   should be unique
  // We assume an array of objects here
  // NOTE: We are skipping any object which
  //   contains a duplicated value for that
  //   particular key.  Make sure this is what
  //   you want!
  return function (arr, targetField) {

    var values = [],
        i, 
        unique,
        l = arr.length, 
        results = [],
        obj;

    // Iterate over all objects in the array
    // and collect all unique values
    for( i = 0; i < arr.length; i++ ) {

      obj = arr[i];

      // check for uniqueness
      unique = true;
      for( v = 0; v < values.length; v++ ){
        if( obj[targetField] == values[v] ){
          unique = false;
        }
      }

      // If this is indeed unique, add its
      //   value to our values and push
      //   it onto the returned array
      if( unique ){
        values.push( obj[targetField] );
        results.push( obj );
      }

    }
    return results;
  };
})

El marcado:

<div ng-repeat = "item in items | unique:'name'">
  {{ item.name }}
</div>
<script src="your/filters.js"></script>
Erik Trautman
fuente
Esto funciona bien pero arroja un error en la consola Cannot read property 'length' of undefineden la líneal = arr.length
Soul Eeater
4

Decidí extender la respuesta de @ thethakuri para permitir cualquier profundidad para el miembro único. Aquí está el código. Esto es para aquellos que no desean incluir todo el módulo AngularUI solo para esta funcionalidad. Si ya está usando AngularUI, ignore esta respuesta:

app.filter('unique', function() {
    return function(collection, primaryKey) { //no need for secondary key
      var output = [], 
          keys = [];
          var splitKeys = primaryKey.split('.'); //split by period


      angular.forEach(collection, function(item) {
            var key = {};
            angular.copy(item, key);
            for(var i=0; i<splitKeys.length; i++){
                key = key[splitKeys[i]];    //the beauty of loosely typed js :)
            }

            if(keys.indexOf(key) === -1) {
              keys.push(key);
              output.push(item);
            }
      });

      return output;
    };
});

Ejemplo

<div ng-repeat="item in items | unique : 'subitem.subitem.subitem.value'"></div>
Kennasoft
fuente
2

Tenía una serie de cadenas, no objetos, y usé este enfoque:

ng-repeat="name in names | unique"

con este filtro:

angular.module('app').filter('unique', unique);
function unique(){
return function(arry){
        Array.prototype.getUnique = function(){
        var u = {}, a = [];
        for(var i = 0, l = this.length; i < l; ++i){
           if(u.hasOwnProperty(this[i])) {
              continue;
           }
           a.push(this[i]);
           u[this[i]] = 1;
        }
        return a;
    };
    if(arry === undefined || arry.length === 0){
          return '';
    }
    else {
         return arry.getUnique(); 
    }

  };
}
Helzgate
fuente
2

ACTUALIZAR

Estaba recomendando el uso de Set, pero lo siento, esto no funciona para ng-repeat, ni Map ya que ng-repeat solo funciona con array. Así que ignora esta respuesta. de todos modos, si necesita filtrar duplicados de una manera, como lo ha dicho otra, usando angular filters, este es el enlace a la sección de inicio .


Vieja respuesta

Puede usar la estructura de datos de conjunto estándar ECMAScript 2015 (ES6) , en lugar de una estructura de datos de matriz de esta manera, filtra los valores repetidos al agregar al conjunto. (Recuerde que los conjuntos no permiten valores repetidos). Muy fácil de usar:

var mySet = new Set();

mySet.add(1);
mySet.add(5);
mySet.add("some text");
var o = {a: 1, b: 2};
mySet.add(o);

mySet.has(1); // true
mySet.has(3); // false, 3 has not been added to the set
mySet.has(5);              // true
mySet.has(Math.sqrt(25));  // true
mySet.has("Some Text".toLowerCase()); // true
mySet.has(o); // true

mySet.size; // 4

mySet.delete(5); // removes 5 from the set
mySet.has(5);    // false, 5 has been removed

mySet.size; // 3, we just removed one value
CommonSenseCode
fuente
2

Aquí hay una forma de hacerlo solo con plantilla (sin embargo, no es mantener el orden). Además, el resultado también se ordenará, lo cual es útil en la mayoría de los casos:

<select ng-model="orderProp" >
   <option ng-repeat="place in places | orderBy:'category' as sortedPlaces" data-ng-if="sortedPlaces[$index-1].category != place.category" value="{{place.category}}">
      {{place.category}}
   </option>
</select>
zapato
fuente
2

Ninguno de los filtros anteriores solucionó mi problema, así que tuve que copiar el filtro del documento oficial de github. Y luego úsalo como se explica en las respuestas anteriores

angular.module('yourAppNameHere').filter('unique', function () {

función de retorno (items, filterOn) {

if (filterOn === false) {
  return items;
}

if ((filterOn || angular.isUndefined(filterOn)) && angular.isArray(items)) {
  var hashCheck = {}, newItems = [];

  var extractValueToCompare = function (item) {
    if (angular.isObject(item) && angular.isString(filterOn)) {
      return item[filterOn];
    } else {
      return item;
    }
  };

  angular.forEach(items, function (item) {
    var valueToCheck, isDuplicate = false;

    for (var i = 0; i < newItems.length; i++) {
      if (angular.equals(extractValueToCompare(newItems[i]), extractValueToCompare(item))) {
        isDuplicate = true;
        break;
      }
    }
    if (!isDuplicate) {
      newItems.push(item);
    }

  });
  items = newItems;
}
return items;
  };

});
Mamba negro
fuente
1

Parece que todos están lanzando su propia versión del uniquefiltro al ring, así que haré lo mismo. La crítica es muy bienvenida.

angular.module('myFilters', [])
  .filter('unique', function () {
    return function (items, attr) {
      var seen = {};
      return items.filter(function (item) {
        return (angular.isUndefined(attr) || !item.hasOwnProperty(attr))
          ? true
          : seen[item[attr]] = !seen[item[attr]];
      });
    };
  });
LS
fuente
1

Si desea obtener datos únicos basados ​​en la clave anidada:

app.filter('unique', function() {
        return function(collection, primaryKey, secondaryKey) { //optional secondary key
          var output = [], 
              keys = [];

          angular.forEach(collection, function(item) {
                var key;
                secondaryKey === undefined ? key = item[primaryKey] : key = item[primaryKey][secondaryKey];

                if(keys.indexOf(key) === -1) {
                  keys.push(key);
                  output.push(item);
                }
          });

          return output;
        };
    });

Llámalo así:

<div ng-repeat="notify in notifications | unique: 'firstlevel':'secondlevel'">
thethakuri
fuente
0

Agregue este filtro:

app.filter('unique', function () {
return function ( collection, keyname) {
var output = [],
    keys = []
    found = [];

if (!keyname) {

    angular.forEach(collection, function (row) {
        var is_found = false;
        angular.forEach(found, function (foundRow) {

            if (foundRow == row) {
                is_found = true;                            
            }
        });

        if (is_found) { return; }
        found.push(row);
        output.push(row);

    });
}
else {

    angular.forEach(collection, function (row) {
        var item = row[keyname];
        if (item === null || item === undefined) return;
        if (keys.indexOf(item) === -1) {
            keys.push(item);
            output.push(row);
        }
    });
}

return output;
};
});

Actualiza tu marcado:

<select ng-model="orderProp" >
   <option ng-repeat="place in places | unique" value="{{place.category}}">{{place.category}}</option>
</select>
Shawn Dotey
fuente
0

Esto puede ser excesivo, pero funciona para mí.

Array.prototype.contains = function (item, prop) {
var arr = this.valueOf();
if (prop == undefined || prop == null) {
    for (var i = 0; i < arr.length; i++) {
        if (arr[i] == item) {
            return true;
        }
    }
}
else {
    for (var i = 0; i < arr.length; i++) {
        if (arr[i][prop] == item) return true;
    }
}
return false;
}

Array.prototype.distinct = function (prop) {
   var arr = this.valueOf();
   var ret = [];
   for (var i = 0; i < arr.length; i++) {
       if (!ret.contains(arr[i][prop], prop)) {
           ret.push(arr[i]);
       }
   }
   arr = [];
   arr = ret;
   return arr;
}

La función distinta depende de la función contiene definida anteriormente. Se puede llamar como array.distinct(prop);donde prop es la propiedad que desea que sea distinta.

Entonces podrías decir $scope.places.distinct("category");

Ikechi Michael
fuente
0

Crea tu propia matriz.

<select name="cmpPro" ng-model="test3.Product" ng-options="q for q in productArray track by q">
    <option value="" >Plans</option>
</select>

 productArray =[];
angular.forEach($scope.leadDetail, function(value,key){
    var index = $scope.productArray.indexOf(value.Product);
    if(index === -1)
    {
        $scope.productArray.push(value.Product);
    }
});
Akhilesh Kumar
fuente