Como fazer ng-repeat filtrar resultados duplicados

131

Estou executando um simples ng-repeatsobre um arquivo JSON e quero obter nomes de categorias. Existem cerca de 100 objetos, cada um pertencendo a uma categoria - mas existem apenas cerca de 6 categorias.

Meu código atual é este:

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

A saída é de 100 opções diferentes, principalmente duplicatas. Como uso o Angular para verificar se um {{place.category}}já existe e não criar uma opção se ele já estiver lá?

edit: no meu javascript $scope.places = JSON data, apenas para esclarecer

JVG
fonte
1
Por que você simplesmente não deduz seus $ scope.places? use jquery map api.jquery.com/map
anazimok 10/04
qual foi a sua solução final de trabalho? É que acima ou há alguma JS fazer o de-duping
mark1234
Eu quero saber a solução para isso. Poste um follow-up. Obrigado!
Jstein1
@ jdstein1 Vou começar com um TLDR: use as respostas abaixo ou use Javascript vanilla para filtrar apenas valores exclusivos em uma matriz. O que fiz: no final, houve um problema com minha lógica e meu entendimento do MVC. Eu estava carregando dados do MongoDB pedindo um despejo de dados e queria que o Angular os filtrasse magicamente para apenas lugares únicos. A solução, como se costuma ser o caso, era deixar de ser preguiçoso e consertar meu modelo de banco de dados - para mim, estava chamando o Mongo db.collection.distinct("places"), que era muito, muito melhor do que fazê-lo dentro do Angular! Infelizmente isso não vai funcionar para todos.
JVG 27/09/15
obrigado pela atualização!
Jstein1

Respostas:

142

Você pode usar o filtro exclusivo da AngularUI (código fonte disponível aqui: filtro exclusivo da AngularUI ) e usá-lo diretamente nas opções ng (ou 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
fonte
33
Para aqueles que não conseguem o filtro exclusivo do AngularUI funcionando: os filtros estão em um módulo separado: por exemplo, você deve incluí-lo como uma referência adicional no seu módulo angular.module('yourModule', ['ui', 'ui.filters']);. Fiquei perplexo até eu dar uma olhada no arquivo js do AngularUI.
precisa saber é o seguinte
8
O uniquefiltro pode actualmente ser encontrados como parte de AngularJS UI Utils
Nick
2
Na nova versão do ui utils, você pode incluir o ui.unique por conta própria. use a instalação do bower apenas para o módulo.
Estatísticas de aprendizado por exemplo
Se você não deseja incluir o AngularUI e sua totalidade, mas deseja usar o filtro exclusivo, copie e cole a fonte unique.js no seu aplicativo e mude angular.module('ui.filters')para o nome do aplicativo.
Chakeda
37

Ou você pode escrever seu próprio filtro usando o lodash.

app.filter('unique', function() {
    return function (arr, field) {
        return _.uniq(arr, function(a) { return a[field]; });
    };
});
Mike Ward
fonte
Olá Mike, parece realmente elegante, mas não parece funcionar como esperado quando passo um filtro como único: status [nome do meu campo], ele retorna apenas o primeiro resultado, em oposição à lista desejada de todas as estátuas únicas disponíveis. Algum palpite porque?
SinSync
3
Embora eu tenha usado sublinhado, essa solução funcionou como um encanto. Obrigado!
Jon Black
Solução muito elegante.
JD Smith #
1
lodash é um garfo de sublinhado. Tem melhor desempenho e mais utilitários.
Mike Ward
2
no lodash v4 _.uniqBy(arr, field);deve funcionar para propriedades aninhadas
ijavid 27/07/16
30

Você pode usar o filtro 'unique' (aliases: uniq) no módulo angular.filter

use: colection | uniq: 'property'
você também pode filtrar por propriedades aninhadas: colection | uniq: 'property.nested_property'

O que você pode fazer, é algo assim ..

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 do cliente, ou seja, removemos 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
fonte
como sua resposta, mas com dificuldade para traduzir para o meu problema. Como você lidaria com uma lista aninhada. Por exemplo, uma lista de pedidos que continha uma lista de itens para cada pedido. No seu exemplo, você tem um cliente por pedido. Gostaria de ver uma lista exclusiva de itens em todos os pedidos. Não para enfatizar o ponto, mas você também pode pensar nele como um cliente com muitos pedidos e um pedido com muitos itens. Como posso mostrar todos os itens anteriores que um cliente encomendou? Pensamentos?
Greg Grater
@GregGrater Você pode fornecer um objeto de exemplo semelhante ao seu problema?
a8m
Obrigado @Ariel M.! Aqui está um exemplo dos dados:
Greg Grater
Com quais versões do angular é compatível?
Icet
15

esse código funciona para mim.

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

e depois

var colors=$filter('unique')(items,"color");
Eduardo Ortiz
fonte
2
A definição de l deve ser l = arr! = Indefinida? arr.Length: 0 caso contrário há um erro de análise em AngularJS
Gerrit
6

Se você deseja listar categorias, acho que deve declarar explicitamente sua intenção na exibição.

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

no controlador:

$scope.categories = $scope.places.reduce(function(sum, place) {
  if (sum.indexOf( place.category ) < 0) sum.push( place.category );
  return sum;
}, []);
Tosh
fonte
você poderia explicar isso um pouco mais? Quero repetir os elementos seguidos para cada categoria exclusiva. O JS simplesmente entra no arquivo JS onde o controlador está definido?
mark1234
Se você deseja agrupar as opções, é uma pergunta diferente. Você pode querer examinar o elemento optgroup. Angular suporta optgroup. procure group byexpressão na selectdiretiva.
Tosh
Isso acontecerá caso um local seja adicionado / removido? A categoria associada será adicionada / removida se for a única instância em alguns locais?
precisa
4

Aqui está um exemplo direto e genérico.

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

A marcação:

<div ng-repeat = "item in items | unique:'name'">
  {{ item.name }}
</div>
<script src="your/filters.js"></script>
Erik Trautman
fonte
Isso funciona bem, mas ele lança um erro na consola Cannot read property 'length' of undefinedna linhal = arr.length
Alma Eeater
4

Decidi estender a resposta de @ thethakuri para permitir profundidade ao membro único. Aqui está o código. Isso é para aqueles que não desejam incluir o módulo AngularUI inteiro apenas para esta funcionalidade. Se você já estiver usando o AngularUI, ignore esta resposta:

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

Exemplo

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

Eu tinha uma matriz de seqüências de caracteres, não objetos e usei esta abordagem:

ng-repeat="name in names | unique"

com 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
fonte
2

ATUALIZAR

Eu estava recomendando o uso de Set, mas desculpe isso não funcionar para ng-repeat, nem Map, já que ng-repeat funciona apenas com array. Portanto, ignore esta resposta. de qualquer forma, se você precisar filtrar duplicatas de uma maneira, como já foi dito angular filters, aqui está o link para a seção de introdução .


Resposta antiga

Você pode usar a estrutura de dados de conjunto padrão do ECMAScript 2015 (ES6) , em vez de uma estrutura de dados de matriz dessa maneira, você filtra valores repetidos ao adicionar ao conjunto. (Lembre-se de que conjuntos não permitem valores repetidos). Muito 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
fonte
2

Aqui está uma maneira apenas de modelo de fazê-lo (embora não esteja mantendo o pedido). Além disso, o resultado também será solicitado, o que é útil na maioria dos 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>
shoesel
fonte
2

Nenhum dos filtros acima corrigiu meu problema, então tive que copiar o filtro do documento oficial do github. E então use-o como explicado nas respostas acima

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

função de retorno (itens, 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 negra
fonte
1

Parece que todo mundo está jogando sua própria versão do uniquefiltro no anel, então eu farei o mesmo. A crítica é muito bem-vinda.

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

Se você deseja obter dados exclusivos com base na chave aninhada:

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

Chame assim:

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

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

Atualize sua marcação:

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

Isso pode ser um exagero, mas funciona para mim.

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

A função distinta depende da função contém definida acima. Pode ser chamado como array.distinct(prop);onde prop é a propriedade que você deseja que seja distinta.

Então você poderia dizer $scope.places.distinct("category");

Ikechi Michael
fonte
0

Crie sua própria 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
fonte