¿Cómo aplicar filtros a * ngFor?


278

Aparentemente, Angular 2 usará tuberías en lugar de filtros como en Angular1 junto con ng-for para filtrar resultados, aunque la implementación aún parece ser vaga, sin documentación clara.

A saber, lo que estoy tratando de lograr podría verse desde la siguiente perspectiva

<div *ng-for="#item of itemsList" *ng-if="conditon(item)"></div>

¿Cómo implementarlo usando tuberías?


8
Tenga en cuenta que se introduce un cambio importante en beta 17 para ngFor con respecto al símbolo hash. La forma correcta es:<div *ngFor="let item of itemsList" *ngIf="conditon(item)" ...
Memet Olsen


11
Comentario de @MemetOlsen de Gunter a continuación: " *ngFory *ngIfen el mismo elemento no son compatibles. Debes cambiar a la forma explícita de uno de ellos"
The Red Pea

1
Aunque es lo que pide el OP, se recomienda NO UTILIZAR TUBO para filtrar u ordenar en Angular2 +. Prefiero tener una propiedad de clase con los valores filtrados: angular.io/guide/pipes#appendix-no-filterpipe-or-orderbypipe
ylerjen

Respuestas:


395

Básicamente, escribe una tubería que luego puede usar en la *ngFordirectiva.

En su componente:

filterargs = {title: 'hello'};
items = [{title: 'hello world'}, {title: 'hello kitty'}, {title: 'foo bar'}];

En su plantilla, puede pasar cadena, número u objeto a su tubería para usar para filtrar:

<li *ngFor="let item of items | myfilter:filterargs">

En tu pipa:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
    name: 'myfilter',
    pure: false
})
export class MyFilterPipe implements PipeTransform {
    transform(items: any[], filter: Object): any {
        if (!items || !filter) {
            return items;
        }
        // filter items array, items which match and return true will be
        // kept, false will be filtered out
        return items.filter(item => item.title.indexOf(filter.title) !== -1);
    }
}

Recuerde registrar su tubería en app.module.ts; ya no necesita registrar las tuberías en su@Component

import { MyFilterPipe } from './shared/pipes/my-filter.pipe';

@NgModule({
    imports: [
        ..
    ],
    declarations: [
        MyFilterPipe,
    ],
    providers: [
        ..
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }

Aquí hay un Plunker que muestra el uso de una tubería de filtro personalizada y la tubería de corte incorporada para limitar los resultados.

Tenga en cuenta (como han señalado varios comentaristas) que hay una razón por la cual no hay tuberías de filtro integradas en Angular.


66
Gracias, esto funciona según lo previsto, pero a veces es mejor verificar si la matriz de elementos está definida y no es nula, porque Ng2 puede intentar aplicar el filtro mientras los "elementos" aún no están definidos.
timmz

1
Además, necesitaba agregar la clase de filtro a la declaración @Component. Me gusta así: @Component ({... tuberías: [MyFilterPipe]
Stephen

1
Creo que también necesita esta línea "si (! Items) devuelve elementos" en caso de que la matriz esté vacía.
Boštjan Pišler

2
Angular dice que usar una tubería tiene problemas de rendimiento, por lo que recomienda hacer un filtrado en el componente
Sebastián Rojas

3
Me gustaría sugerir envolver los *ngForparámetros entre paréntesis, solo para evitar cualquier confusión y hacer que sea "a prueba de cambios":<li *ngFor="let item of (items | myfilter:filterargs)">
Tomás

104

Muchos de ustedes tienen excelentes enfoques, pero el objetivo aquí es ser genéricos y definir una tubería de matriz que sea extremadamente reutilizable en todos los casos en relación con * ngFor.

callback.pipe.ts (no olvide agregar esto a la matriz de declaración de su módulo)

import { PipeTransform, Pipe } from '@angular/core';

@Pipe({
    name: 'callback',
    pure: false
})
export class CallbackPipe implements PipeTransform {
    transform(items: any[], callback: (item: any) => boolean): any {
        if (!items || !callback) {
            return items;
        }
        return items.filter(item => callback(item));
    }
}

Luego, en su componente, debe implementar un método con la siguiente firma (item: any) => boolean , en mi caso, por ejemplo, lo llamé filterUser, que filtra la edad de los usuarios que es mayor de 18 años.

Su componente

@Component({
  ....
})
export class UsersComponent {
  filterUser(user: IUser) {
    return !user.age >= 18
  }
}

Y por último, pero no menos importante, su código html se verá así:

Su HTML

<li *ngFor="let user of users | callback: filterUser">{{user.name}}</li>

Como puede ver, este Pipe es bastante genérico en todos los elementos similares a la matriz que deben filtrarse mediante una devolución de llamada. En mi caso, me pareció muy útil para * ngPara escenarios similares.

¡¡¡Espero que esto ayude!!!

codematrix


44
Noté que en la función filterUser (), o en mi función equivalente a esa, no puede usar "this" para acceder a la instancia del componente actual como puede hacerlo con todas las demás funciones en la clase de componente. Necesito acceder al objeto componente para verificar que el elemento filtrado esté en una colección.
Paul

1
@ Paul, hmm ... eso es imposible. ¿Es tu método privado? Esto no debería importar, ya que los elementos privados son solo compilaciones y no se aplican en tiempo de ejecución. En mi ejemplo usé IUser. Esto supone que los elementos de la colección que se está iterando se asignan a ella. Puedes probar cualquiera para ver si funciona. Además, asegúrese de que el nombre esté escrito correctamente, mayúsculas y minúsculas.
código5

1
No puedo acceder a la variable de componente utilizando este método
suulisin

10
Para evitar el problema de thisser indefinido, puede escribir su método en su componente como en filterUser = (user: IUser) =>lugar defilteruser(user: IUser)
Tom

2
@Paul Sé que es demasiado tarde para ayudarte, pero podría ayudar a otros. La razón por la que estaba perdiendo thissu método componente es porque el método se estaba utilizando como devolución de llamada y thisse aplicó un nuevo contexto. Te encontraste con un problema común en JavaScript orientado a objetos, pero hay una solución antigua y fácil: unes métodos para usarlos como devoluciones de llamada a la clase original. En su constructor, agregue el siguiente código: this.myCallbackFunc = this.myCallbackFunc.bind(this); Eso es todo. Nunca volverás a perder this.
Randolpho

36

Forma simplificada (se usa solo en matrices pequeñas debido a problemas de rendimiento. En matrices grandes, debe hacer el filtro manualmente mediante código):

Ver: https://angular.io/guide/pipes#appendix-no-filterpipe-or-orderbypipe

@Pipe({
    name: 'filter'
})
@Injectable()
export class FilterPipe implements PipeTransform {
    transform(items: any[], field : string, value : string): any[] {  
      if (!items) return [];
      if (!value || value.length == 0) return items;
      return items.filter(it => 
      it[field].toLowerCase().indexOf(value.toLowerCase()) !=-1);
    }
}

Uso:

<li *ngFor="let it of its | filter : 'name' : 'value or variable'">{{it}}</li>

Si usa una variable como segundo argumento, no use comillas.


3
Tal vez agregue lo siguiente para mostrar cómo combinarlo con ReqExp: return items.filter (item => {return new RegExp (value, "i"). Test (item [field])});
Johannes

8
Según el equipo de Angular, esto se considera una mala práctica.

@torazaburo, ¿puede hacer referencia a su opinión o explicar por qué? Gracias
Zymotik


2
Según el equipo de Angular, este es un código deficiente porque es lento y no se minimiza bien. El equipo no quiere ver sitios web lentos debido a su código, por lo que esta vez no lo incorporaron a Angular. angular.io/docs/ts/latest/guide/…
Zymotik

29

Esto es lo que implementé sin usar pipe.

componente.html

<div *ngFor="let item of filter(itemsList)">

componente.ts

@Component({
....
})
export class YourComponent {
  filter(itemList: yourItemType[]): yourItemType[] {
    let result: yourItemType[] = [];
    //your filter logic here
    ...
    ...
    return result;
  }
}

16
Creo que esto sería computacionalmente intensivo porque Angular ejecutará el filtro cada vez que ejecute la detección de cambios. No escalará bien a grandes matrices. Un limpiador, aunque más complejo de código, la solución sería hacer itemListun observable y utilizar el filtro asíncrono: let item of itemsList | async. Cuando ocurre un cambio, haga que el observable emita la nueva lista. De esta manera, el código de filtrado solo se ejecuta cuando es necesario.
BeetleJuice

1
Esta respuesta debería tener una puntuación negativa. Es malo, usa una pipa.
Cétia


11

También puede usar lo siguiente:

<template ngFor let-item [ngForOf]="itemsList">
    <div *ng-if="conditon(item)"></div>
</template>

Esto solo mostrará el div si sus artículos coinciden con la condición

Consulte la documentación angular para obtener más información. Si también necesita el índice, utilice lo siguiente:

<template ngFor let-item [ngForOf]="itemsList" let-i="index">
    <div *ng-if="conditon(item, i)"></div>
</template>

1
¿Esto no ingresará la plantilla para cada elemento de la lista en lugar de solo la lista filtrada? Eso podría ser un éxito de rendimiento.
Azeroth2b

8

Las tuberías en Angular2 son similares a las tuberías en la línea de comando. La salida de cada valor precedente se alimenta al filtro después de la tubería, lo que facilita el encadenamiento de los filtros así:

<template *ngFor="#item of itemsList">
    <div *ngIf="conditon(item)">{item | filter1 | filter2}</div>
</template>

Lo siento si esto fue engañoso, mi punto aquí es que la variable itemde *ng-for="#item of itemsList"debe usarse para filtrar los resultados como tal *ng-if="conditon(item)". Lo que no funciona en este ejemplo ..
Khaled

podrías hacer que condición sea un filtro y hacer lo mismo con {{item | condición}} la condición solo regresaría itemsi se cumple la condición y no tiene valor si no se cumple.
Ben Glasser

@BenGlasser Pensé que las tuberías se aplicaban de derecha a izquierda. Entonces esto aplicaría primero filter2, luego filter1.
Evan Plaice el

12
*ngFory *ngIfen el mismo elemento no son compatibles. <template ngFor ...>
Debe

1
@ GünterZöchbauer Me tomó un año, pero he actualizado la sintaxis para reflejar los cambios que sugirió
Ben Glasser,

5

Para este requisito, implemento y publico un componente genérico . Ver

https://www.npmjs.com/package/w-ng5

Para usar estos componentes, antes, instale este paquete con npm:

npm install w-ng5 --save

Después, importe el módulo en app.module

...
import { PipesModule } from 'w-ng5';

En el siguiente paso, agregue en la sección declarar de app.module:

imports: [
  PipesModule,
  ...
]

Uso de la muestra

Filtrar cadenas simples

<input type="text"  [(ngModel)]="filtroString">
<ul>
  <li *ngFor="let s of getStrings() | filter:filtroString">
    {{s}}
  </li>
</ul>

Filtrado de cadenas complejas: campo 'Valor' en el nivel 2

<input type="text"  [(ngModel)]="search">
<ul>
  <li *ngFor="let s of getComplexTypesExtends() | filter:[{field:'n1.n2.valor2', value: search}]">
    {{s.nome}} - {{s.idade}} - {{s.n1.valor1}} - {{s.n1.n2.valor2}}
  </li>
</ul>

Filtrado de cadenas complejas - campo medio - 'Valor' en el nivel 1

<input type="text"  [(ngModel)]="search3">
<ul>
  <li *ngFor="let s of getComplexTypesExtends() | filter:[{field:'n1.valor1', value: search3}]">
    {{s.nome}} - {{s.idade}} - {{s.n1.valor1}} - {{s.n1.n2.valor2}}
  </li>
</ul>

Filtrado de matriz compleja simple - campo 'Nome' nivel 0

<input type="text"  [(ngModel)]="search2">
<ul>
  <li *ngFor="let s of getComplexTypesExtends() | filter:[{field:'nome', value: search2}]">
    {{s.nome}} - {{s.idade}} - {{s.n1.valor1}} - {{s.n1.n2.valor2}}
  </li>
</ul>

Filtrado en campos de árbol: campo 'Valor' en el nivel 2 o 'Valor' en el nivel 1 o 'Nome' en el nivel 0

<input type="text"  [(ngModel)]="search5">
<ul>
  <li *ngFor="let s of getComplexTypesExtends() | filter:[{field:'n1.n2.valor2', value: search5}, {field:'n1.valor1', value: search5}, {field:'nome', value: search5}]">
    {{s.nome}} - {{s.idade}} - {{s.n1.valor1}} - {{s.n1.n2.valor2}}
  </li>
</ul>

Filtrado de campo inexistente - 'Valor' en nivel 3 inexistente

<input type="text"  [(ngModel)]="search4">
<ul>
  <li *ngFor="let s of getComplexTypesExtends() | filter:[{field:'n1.n2.n3.valor3', value: search4}]">
    {{s.nome}} - {{s.idade}} - {{s.n1.valor1}} - {{s.n1.n2.valor2}}
  </li>
</ul>

Este componente funciona con un nivel de atributo infinito ...


Hola, estoy aquí y he seguido todos los pasos y en este caso estoy usando esto *ngFor="let inovice of invoices | filter:searchInvoice"y está buscando en mi lista, pero muestra una lista en blanco, ¿sabes por qué?
jecorrales el

1
Hola, dígame cuál es la estructura y el tipo de objetos que contiene su lista de facturas. La forma en que lo usa solo debe aplicarse si su lista de facturas es de tipo cadena. Si desea buscar por número de factura (invoice.number), use esto: * ngFor = "let inovice of invoices | filter: {field: number, value: searchInvoice}" . Si desea filtrar por dos columnas, por ejemplo, invoice.customer.name, use: * ngFor = "let inovice of invoices | filter: [field: number, value: searchInvoice}, {field: customer.name, value: searchInvoice}] .
Wedson Quintanilha da Silva

4

Una solución simple que funciona con Angular 6 para filtrar un ngFor, es la siguiente:

<span *ngFor="item of itemsList"  >
  <div *ngIf="yourCondition(item)">
    
    your code
    
  </div>
</span

Los tramos son útiles porque no representan inherentemente nada.


1
mejor que <span> es usar <ng-container> ya que no agregará ningún marcado innecesario que, además del ruido html, podría afectar su CSS.
Trevor de Koekkoek

4

Sé que es una vieja pregunta, sin embargo, pensé que podría ser útil ofrecer otra solución.

equivalente de AngularJS de esto

<div *ng-for="#item of itemsList" *ng-if="conditon(item)"></div>

en Angular 2+ no puede usar * ngFor y * ngIf en un mismo elemento, por lo que será el siguiente:

<div *ngFor="let item of itemsList">
     <div *ngIf="conditon(item)">
     </div>
</div>

y si no puede usarlo como contenedor interno, use ng-container en su lugar. ng-container es útil cuando desea agregar condicionalmente un grupo de elementos (es decir, usando * ngIf = "foo") en su aplicación, pero no desea envolverlos con otro elemento.


4

He creado un plunker basado en las respuestas aquí y en otros lugares.

Además he tenido que añadir una @Input, @ViewChildy ElementRefde la <input>y crear ysubscribe() a un observable de la misma.

Filtro de búsqueda Angular2: PLUNKR (ACTUALIZACIÓN: plunker ya no funciona)


3

La tubería sería el mejor enfoque. pero debajo de uno también funcionaría.

<div *ng-for="#item of itemsList">
  <ng-container *ng-if="conditon(item)">
    // my code
  </ng-container>
</div>

Esto puede romper ciertas cosas. por ejemplo dentro de un campo mat-form
pcnate

2

Este es mi código:

import {Pipe, PipeTransform, Injectable} from '@angular/core';

@Pipe({
    name: 'filter'
})
@Injectable()
export class FilterPipe implements PipeTransform {
    transform(items: any[], field : string, value): any[] {
      if (!items) return [];
      if (!value || value.length === 0) return items;
      return items.filter(it =>
      it[field] === value);
    }
}

Muestra:

LIST = [{id:1,name:'abc'},{id:2,name:'cba'}];
FilterValue = 1;

<span *ngFor="let listItem of LIST | filter : 'id' : FilterValue">
                              {{listItem .name}}
                          </span>

1

Otro enfoque que me gusta usar para los filtros específicos de la aplicación es usar una propiedad personalizada de solo lectura en su componente que le permita encapsular la lógica de filtrado de manera más limpia que usar una tubería personalizada (IMHO).

Por ejemplo, si quiero enlazar albumListy filtrar searchText:

searchText: "";
albumList: Album[] = [];

get filteredAlbumList() {
    if (this.config.searchText && this.config.searchText.length > 1) {
      var lsearchText = this.config.searchText.toLowerCase();
      return this.albumList.filter((a) =>
        a.Title.toLowerCase().includes(lsearchText) ||
        a.Artist.ArtistName.toLowerCase().includes(lsearchText)
      );
    }
    return this.albumList;
}

Para enlazar el HTML, puede enlazar a la propiedad de solo lectura:

<a class="list-group-item"
       *ngFor="let album of filteredAlbumList">
</a>

Creo que para los filtros especializados que son específicos de la aplicación, esto funciona mejor que una tubería, ya que mantiene la lógica relacionada con el filtro con el componente.

Las tuberías funcionan mejor para filtros reutilizables a nivel mundial.


1
¿No activará este método las comprobaciones sucias continuas en lugar de utilizar un enfoque valueChanged?
Léon Pelletier

1

Creé la siguiente tubería para obtener los elementos deseados de una lista.

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'filter'
})
export class FilterPipe implements PipeTransform {

  transform(items: any[], filter: string): any {
    if(!items || !filter) {
      return items;
    }
    // To search values only of "name" variable of your object(item)
    //return items.filter(item => item.name.toLowerCase().indexOf(filter.toLowerCase()) !== -1);

    // To search in values of every variable of your object(item)
    return items.filter(item => JSON.stringify(item).toLowerCase().indexOf(filter.toLowerCase()) !== -1);
  }

}

La conversión en minúsculas es solo para coincidir en mayúsculas y minúsculas. Puede usarlo en su vista así: -

<div>
  <input type="text" placeholder="Search reward" [(ngModel)]="searchTerm">
</div>
<div>
  <ul>
    <li *ngFor="let reward of rewardList | filter:searchTerm">
      <div>
        <img [src]="reward.imageUrl"/>
        <p>{{reward.name}}</p>
      </div>
    </li>
  </ul>
</div>

1

Idealmente, debe crear una tubería angualr 2 para eso. Pero puedes hacer este truco.

<ng-container *ngFor="item in itemsList">
    <div*ngIf="conditon(item)">{{item}}</div>
</ng-container>

1

Basado en la solución de tubería de devolución de llamada muy elegante propuesta anteriormente, es posible generalizarlo un poco más permitiendo que se pasen parámetros de filtro adicionales. Entonces tenemos:

callback.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'callback',
  pure: false
})
export class CallbackPipe implements PipeTransform {
  transform(items: any[], callback: (item: any, callbackArgs?: any[]) => boolean, callbackArgs?: any[]): any {
    if (!items || !callback) {
      return items;
    }
    return items.filter(item => callback(item, callbackArgs));
  }
}

componente

filterSomething(something: Something, filterArgs: any[]) {
  const firstArg = filterArgs[0];
  const secondArg = filterArgs[1];
  ...
  return <some condition based on something, firstArg, secondArg, etc.>;
}

html

<li *ngFor="let s of somethings | callback : filterSomething : [<whatWillBecomeFirstArg>, <whatWillBecomeSecondArg>, ...]">
  {{s.aProperty}}
</li>

0

Aquí hay un ejemplo que creé hace un tiempo, y en el que escribí en un blog, que incluye un golpe de trabajo. Proporciona una tubería de filtro que puede filtrar cualquier lista de objetos. Básicamente, solo especifica la propiedad y el valor {clave: valor} dentro de su especificación ngFor.

No es muy diferente de la respuesta de @ NateMay, excepto que lo explico con un detalle relativamente detallado.

En mi caso, filtré una lista desordenada en algún texto (filterText) que el usuario ingresó contra la propiedad "etiqueta" de los objetos en mi matriz con este tipo de marcado:

<ul>
  <li *ngFor="let item of _items | filter:{label: filterText}">{{ item.label }}</li>
</ul>

https://long2know.com/2016/11/angular2-filter-pipes/


0

El primer paso que crea Filter usando @Pipeen su archivo component.ts:

your.component.ts

import { Component, Pipe, PipeTransform, Injectable } from '@angular/core';
import { Person} from "yourPath";

@Pipe({
  name: 'searchfilter'
})
@Injectable()
export class SearchFilterPipe implements PipeTransform {
  transform(items: Person[], value: string): any[] {
    if (!items || !value) {
      return items;
    }
    console.log("your search token = "+value);
    return items.filter(e => e.firstName.toLowerCase().includes(value.toLocaleLowerCase()));
  }
}
@Component({
  ....
    persons;

    ngOnInit() {
         //inicial persons arrays
    }
})

Y estructura de datos del objeto Persona:

person.ts

export class Person{
    constructor(
        public firstName: string,
        public lastName: string
    ) { }
}

En su vista en el archivo html:

your.component.html

    <input class="form-control" placeholder="Search" id="search" type="text" [(ngModel)]="searchText"/>
    <table class="table table-striped table-hover">
      <colgroup>
        <col span="1" style="width: 50%;">
        <col span="1" style="width: 50%;">
      </colgroup>
      <thead>
        <tr>
          <th>First name</th>
          <th>Last name</th>
        </tr>
      </thead>
      <tbody>
        <tr *ngFor="let person of persons | searchfilter:searchText">
          <td>{{person.firstName}}</td>
          <td>{{person.lastName}}</td>
        </tr>
      </tbody>
    </table>

0

Esta es tu matriz

products: any = [
        {
            "name": "John-Cena",
                    },
        {
            "name": "Brock-Lensar",

        }
    ];

Este es tu ngFor loop Filtrar por:

<input type="text" [(ngModel)]='filterText' />
    <ul *ngFor='let product of filterProduct'>
      <li>{{product.name }}</li>
    </ul>

Allí estoy usando filterProduct instantáneo de productos, porque quiero preservar mis datos originales. Aquí el modelo _filterText se usa como un cuadro de entrada. Cuando haya alguna función de cambio, se llamará. En setFilterText se llama a performProduct, devolverá el resultado solo a aquellos que coincidan con la entrada. Estoy usando minúsculas para mayúsculas y minúsculas.

filterProduct = this.products;
_filterText : string;
    get filterText() : string {
        return this._filterText;
    }

    set filterText(value : string) {
        this._filterText = value;
        this.filterProduct = this._filterText ? this.performProduct(this._filterText) : this.products;

    } 

    performProduct(value : string ) : any {
            value = value.toLocaleLowerCase();
            return this.products.filter(( products : any ) => 
                products.name.toLocaleLowerCase().indexOf(value) !== -1);
        }

0

Después de buscar en Google, me encontré ng2-search-filter. In tomará su objeto y aplicará el término de búsqueda a todas las propiedades del objeto en busca de una coincidencia.


0

Estaba buscando algo para hacer que un filtro pase un objeto, luego puedo usarlo como filtro múltiple: Ejemplo de filtro múltiple

Hice esta solución de belleza:

filter.pipe.ts

import { PipeTransform, Pipe } from '@angular/core';

@Pipe({
  name: 'filterx',
  pure: false
})
export class FilterPipe implements PipeTransform {
 transform(items: any, filter: any, isAnd: boolean): any {
  let filterx=JSON.parse(JSON.stringify(filter));
  for (var prop in filterx) {
    if (Object.prototype.hasOwnProperty.call(filterx, prop)) {
       if(filterx[prop]=='')
       {
         delete filterx[prop];
       }
    }
 }
if (!items || !filterx) {
  return items;
}

return items.filter(function(obj) {
  return Object.keys(filterx).every(function(c) {
    return obj[c].toLowerCase().indexOf(filterx[c].toLowerCase()) !== -1
  });
  });
  }
}

componente.ts

slotFilter:any={start:'',practitionerCodeDisplay:'',practitionerName:''};

componet.html

             <tr>
                <th class="text-center">  <input type="text" [(ngModel)]="slotFilter.start"></th>
                <th class="text-center"><input type="text" [(ngModel)]="slotFilter.practitionerCodeDisplay"></th>
                <th class="text-left"><input type="text" [(ngModel)]="slotFilter.practitionerName"></th>
                <th></th>
              </tr>


 <tbody *ngFor="let item of practionerRoleList | filterx: slotFilter">...
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.