Neste tutorial, usaremos o JavaScript vanilla para implementar um recurso de vários filtros, que permite ao usuário selecionar e combinar vários tipos de filtros diferentes.
A filtragem é um recurso comumente usado na maioria dos sites centrados no usuário, como sites de comércio eletrônico ou páginas de blog. Ele permite que um usuário restrinja o conteúdo usando condições específicas.
Em um tutorial anterior, vimos como filtrar dados de uma lista usando um único parâmetro.
Esse método serviu como uma boa introdução sobre como filtrar dados em uma lista, mas a maioria dos casos da vida real requer um método de filtragem mais complexo.
Pegue um site que vende roupas. Os usuários precisam filtrar por cor, tamanho, tipo de produto, etc. Também é possível selecionar diferentes filtros na mesma categoria. Por exemplo, na demonstração acima recriando uma página de artigo, um usuário pode querer ver todos os artigos marcados com “JavaScript” e “CSS” em vez de apenas uma marca.
1. Layout e estilo
Usaremos o mesmo layout e estilo do tutorial anterior e atualizaremos nosso HTML para incluir um Categorias e Nível recipiente. Também incluiremos um elemento de contagem de artigos após o título de nossos artigos e um contêiner para exibir uma mensagem “nenhum resultado encontrado”. Este é o nosso HTML atualizado.
1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
Tutorials ( id="post-count"https://webdesign.tutsplus.com/tutorials/>)
|
8 |
|
9 |
|
10 |
|
11 |
|
2. Exibindo dados com JavaScript
Em um tutorial anterior, discutimos a extração de dados da página do autor do Tuts+ para criar um objeto de dados simulado para nossa demonstração. Estaremos usando a API Fetch para recuperar os dados copiados armazenados em uma essência do Github.
É assim que nosso objeto de dados se parece:
1 |
[
|
2 |
{
|
3 |
"https://webdesign.tutsplus.com/tutorials/title"https://webdesign.tutsplus.com/tutorials/: ""https://webdesign.tutsplus.com/tutorials/, |
4 |
"https://webdesign.tutsplus.com/tutorials/link"https://webdesign.tutsplus.com/tutorials/: ""https://webdesign.tutsplus.com/tutorials/, |
5 |
"https://webdesign.tutsplus.com/tutorials/image"https://webdesign.tutsplus.com/tutorials/: ""https://webdesign.tutsplus.com/tutorials/, |
6 |
"https://webdesign.tutsplus.com/tutorials/categories"https://webdesign.tutsplus.com/tutorials/: [""https://webdesign.tutsplus.com/tutorials/], |
7 |
"https://webdesign.tutsplus.com/tutorials/level"https://webdesign.tutsplus.com/tutorials/: "" |
8 |
},
|
9 |
...
|
10 |
]
|
Este é o script para buscar dados do script:
1 |
fetch("https://webdesign.tutsplus.com/tutorials/https://gist.githubusercontent.com/jemimaabu/564beec0a30dbd7d63a90a153d2bc80b/raw/12741cce73c71c179381cc9e3b5f79988ea76a45/tutorial-levels" |
2 |
).then(async (response) => { |
3 |
// handle response data
|
4 |
});
|
Depois de obter nossos dados buscados, podemos manipular os dados e anexá-los à página.
3. Anexando dados à página da Web
Para cada objeto em nossa resposta buscada, criaremos um post div que exibirá os dados na página. Primeiro, vamos definir nossas variáveis globais.
vamos criar um postsData
variável para armazenar nossos dados buscados.
1 |
let postsData = ""https://webdesign.tutsplus.com/tutorials/; |
Como este tutorial usa vários filtros, também precisaremos de um currentFilters
variável para armazenar quais filtros estão selecionados no momento:
1 |
let currentFilters = { |
2 |
categories: [], |
3 |
level: [] |
4 |
};
|
Agora vamos criar variáveis para nossos elementos existentes:
1 |
const postsContainer = document.querySelector("https://webdesign.tutsplus.com/tutorials/#posts-container"https://webdesign.tutsplus.com/tutorials/); |
2 |
const categoriesContainer = document.querySelector("https://webdesign.tutsplus.com/tutorials/#post-categories"https://webdesign.tutsplus.com/tutorials/); |
3 |
const levelsContainer = document.querySelector("https://webdesign.tutsplus.com/tutorials/#post-levels"https://webdesign.tutsplus.com/tutorials/); |
4 |
const postCount = document.querySelector("https://webdesign.tutsplus.com/tutorials/#post-count"https://webdesign.tutsplus.com/tutorials/); |
5 |
const noResults = document.querySelector("https://webdesign.tutsplus.com/tutorials/#no-results"https://webdesign.tutsplus.com/tutorials/); |
Então vamos criar uma função createPost()
que lidará com o acréscimo de um novo div ao postsContainer
elemento. Nesta função, criamos um novo elemento div com o nome de classe “post” e definimos o innerHTML como os dados que queremos exibir.
1 |
const createPost = (postData) => { |
2 |
const { title, link, image, categories, level } = postData; |
3 |
const post = document.createElement("https://webdesign.tutsplus.com/tutorials/div"https://webdesign.tutsplus.com/tutorials/); |
4 |
post.className = "https://webdesign.tutsplus.com/tutorials/post"https://webdesign.tutsplus.com/tutorials/; |
5 |
post.innerHTML = ` |
6 |
${link}" target="_blank">
|
7 |
${image}"> |
8 |
|
9 |
|
10 |
${title} |
11 |
|
12 |
${categories |
13 |
.map((category) => { |
14 |
return "https://webdesign.tutsplus.com/tutorials/' + category + "https://webdesign.tutsplus.com/tutorials/"https://webdesign.tutsplus.com/tutorials/; |
15 |
})
|
16 |
.join(""https://webdesign.tutsplus.com/tutorials/)} |
17 |
|
18 |
|
19 |
${level}
|
20 |
|
21 |
|
22 |
`; |
23 |
|
24 |
postsContainer.append(post); |
25 |
};
|
Dentro de nosso post innerHTML, usamos o join("")
método em nosso categories.map()
para remover o símbolo ‘,’ incluído em cada array.
Agora podemos atualizar nossa função de resposta para chamar o createPost()
função uma vez que os dados foram buscados e também atualizar nosso postCount
elemento:
1 |
fetch("https://webdesign.tutsplus.com/tutorials/https://gist.githubusercontent.com/jemimaabu/b89339c1b7e5f81f8737fb66a858b6fc/raw/cdded4a10dbc98858481b5aedbcce3f3026dc271/tutorials" |
2 |
).then(async (response) => { |
3 |
postsData = await response.json(); |
4 |
postsData.map((post) => createPost(post)); |
5 |
postCount.innerText = postsData.length; |
6 |
});
|
4. Obter parâmetros de filtro da resposta
Como estamos usando JavaScript, podemos mapear nossa resposta para criar uma lista dinâmica de parâmetros de filtro.
Escreveremos um script que classifique os categories
e level
keys em cada objeto de resposta e retorna uma lista exclusiva. Podemos atualizar nosso objeto de resposta para lidar com a obtenção de uma lista exclusiva de parâmetros de filtro.
Uma coisa a notar é que o categories
e level
as chaves são de tipos de dados diferentes, portanto, precisam ser tratadas de maneira diferente. Categories é um array e level é uma string.
1 |
fetch( |
2 |
"https://webdesign.tutsplus.com/tutorials/https://gist.githubusercontent.com/jemimaabu/564beec0a30dbd7d63a90a153d2bc80b/raw/12741cce73c71c179381cc9e3b5f79988ea76a45/tutorial-levels" |
3 |
).then(async (response) => { |
4 |
postsData = await response.json(); |
5 |
postsData.map((post) => createPost(post)); |
6 |
postCount.innerText = postsData.length; |
7 |
|
8 |
categoriasDados = [ |
9 |
...new Set( |
10 |
postsData
|
11 |
.map((post) => post.categories) |
12 |
.reduce((acc, curVal) => acc.concat(curVal), []) |
13 |
)
|
14 |
];
|
15 |
|
16 |
levelData = [...new Set(postsData.map((post) => post.level))]; |
17 |
});
|
Dividindo o código para nossas categoriasData:
- Nós usamos
[... new Set]
para criar uma matriz de valores exclusivos. Set retorna um objeto de valores únicos e a sintaxe de propagação […] converte o objeto em um array. - Mapeamos os postsData para obter a matriz de categorias de cada objeto post dentro da resposta de dados.
- Nós usamos o
.reduce()
método para combinar a matriz de categorias para cada objeto de postagem em uma matriz.
Como o nível é uma string, não precisamos reduzi-lo a um array, então podemos apenas criar um novo array usando o Set
objeto e map
método.
Depois de obter nossa matriz de valores de filtro exclusivos dos resultados da postagem, podemos criar uma função para anexar cada filtro à página.
Criaremos um novo elemento de botão e definiremos o innerText de acordo com o valor do filtro. Também precisaremos levar em consideração que tipo de filtro é (um filtro de categoria ou um filtro de nível).
Cada botão de filtro terá um ouvinte de evento de clique definido para o handleButtonClick
função, que será responsável por lidar com a lógica de filtragem. Também definiremos um atributo “data-state” para lidar com a alteração do estado do botão quando clicado.
1 |
const createFilter = (key, param, container) => { |
2 |
const filterButton = document.createElement("https://webdesign.tutsplus.com/tutorials/button"https://webdesign.tutsplus.com/tutorials/); |
3 |
filterButton.className = "https://webdesign.tutsplus.com/tutorials/filter-button"https://webdesign.tutsplus.com/tutorials/; |
4 |
filterButton.innerText = param; |
5 |
filterButton.setAttribute("https://webdesign.tutsplus.com/tutorials/data-state"https://webdesign.tutsplus.com/tutorials/, "https://webdesign.tutsplus.com/tutorials/inactive"https://webdesign.tutsplus.com/tutorials/); |
6 |
filterButton.addEventListener("https://webdesign.tutsplus.com/tutorials/click"https://webdesign.tutsplus.com/tutorials/, (e) => |
7 |
handleButtonClick(e, key, param, container) |
8 |
);
|
9 |
|
10 |
container.append(filterButton); |
11 |
};
|
createFilter
pega os seguintes parâmetros e os passa para o handleButtonClick
função:
-
key
: A chave de filtro do objeto de resposta (categoria ou nível) -
param
: O valor correspondente da chave (por exemplo, postar[0].level=”Iniciante”) -
container
: o elemento contêiner ao qual o filtro será anexado.
A função de resposta agora pode ser atualizada para chamar o createFilter()
função:
1 |
fetch( |
2 |
"https://webdesign.tutsplus.com/tutorials/https://gist.githubusercontent.com/jemimaabu/564beec0a30dbd7d63a90a153d2bc80b/raw/12741cce73c71c179381cc9e3b5f79988ea76a45/tutorial-levels" |
3 |
).then(async (response) => { |
4 |
postsData = await response.json(); |
5 |
postsData.map((post) => createPost(post)); |
6 |
postCount.innerText = postsData.length; |
7 |
|
8 |
categoriasDados = [ |
9 |
...new Set( |
10 |
postsData
|
11 |
.map((post) => post.categories) |
12 |
.reduce((acc, curVal) => acc.concat(curVal), []) |
13 |
)
|
14 |
];
|
15 |
categoriesData.map((category) => |
16 |
createFilter("https://webdesign.tutsplus.com/tutorials/categories"https://webdesign.tutsplus.com/tutorials/, category, categoriesContainer) |
17 |
);
|
18 |
|
19 |
levelData = [...new Set(postsData.map((post) => post.level))]; |
20 |
levelData.map((level) => createFilter("https://webdesign.tutsplus.com/tutorials/level"https://webdesign.tutsplus.com/tutorials/, level, levelsContainer)); |
21 |
});
|
5. Lidar com o clique do botão
Agora que obtivemos nossos botões de filtro e dados iniciais, podemos definir uma função para lidar com a filtragem dos dados quando um botão é clicado. É aqui que a maior parte do nosso componente de filtragem será tratada.
Queremos detectar quando um botão foi clicado e atualizar o estado do botão. Neste tutorial, alternaremos os botões para que, se clicados uma vez, o botão seja definido como ativo e se clicado novamente, o botão é definido como inativo. Vamos detalhar como lidaremos com o clique do botão:
- Se um botão inativo for clicado, adicionamos o parâmetro atual à chave apropriada em nosso
currentFilters
objeto. Por exemplo, se o botão de filtro Animação em Categorias é clicado, nosso objeto currentFilters deve se parecer com{ categories: ['Animation'], level: [] };
Também atualizaremos o estilo do botão para seu estado ativo. - Se o botão já estiver ativo, vamos defini-lo como inativo e remover o parâmetro da tecla correspondente em
currentFilters
. Podemos fazer isso filtrando a chave currentFilters. - Quando atualizarmos o estado do botão, cuidaremos da filtragem de postagens com base nos valores do
currentFilters
objeto.
Agora podemos definir nosso handleButtonClick
função:
1 |
const handleButtonClick = (e, key, param, container) => { |
2 |
const button = e.target; |
3 |
const buttonState = button.getAttribute("https://webdesign.tutsplus.com/tutorials/data-state"https://webdesign.tutsplus.com/tutorials/); |
4 |
if (buttonState == "https://webdesign.tutsplus.com/tutorials/inactive"https://webdesign.tutsplus.com/tutorials/) { |
5 |
button.classList.add("https://webdesign.tutsplus.com/tutorials/is-active"https://webdesign.tutsplus.com/tutorials/); |
6 |
button.setAttribute("https://webdesign.tutsplus.com/tutorials/data-state"https://webdesign.tutsplus.com/tutorials/, "https://webdesign.tutsplus.com/tutorials/active"https://webdesign.tutsplus.com/tutorials/); |
7 |
currentFilters[key].push(param); |
8 |
handleFilterPosts(currentFilters); |
9 |
} else { |
10 |
button.classList.remove("https://webdesign.tutsplus.com/tutorials/is-active"https://webdesign.tutsplus.com/tutorials/); |
11 |
button.setAttribute("https://webdesign.tutsplus.com/tutorials/data-state"https://webdesign.tutsplus.com/tutorials/, "https://webdesign.tutsplus.com/tutorials/inactive"https://webdesign.tutsplus.com/tutorials/); |
12 |
currentFilters[key] = currentFilters[key].filter((item) => item !== param); |
13 |
handleFilterPosts(currentFilters); |
14 |
}
|
15 |
};
|
Vamos dar uma olhada em como os currentFilters são atualizados com base no estado do botão:
6. Definindo multifiltros
Uma coisa a ter em mente ao manipular dados é evitar a mutação. Queremos garantir que nosso array original não seja alterado durante a filtragem.
Primeiro, criaremos uma nova instância de nossa variável postsData chamada filteredPosts
para evitar a mutação da matriz original.
1 |
let filteredPosts = [...postsData]; |
Agora podemos trabalhar na lógica de filtragem. Lembre-se de que nosso postsData retorna dois tipos de dados diferentes para os filtros: categorias é uma matriz e nível é uma string.
Queremos garantir que nosso componente de filtro funcione para esses dois tipos de dados diferentes.
Para a matriz de categorias em currentFilter, primeiro precisamos verificar se a matriz não está vazia usando filters.categories.length > 0
Agora estaremos filtrando as categorias de postagem pelas categorias currentFilters. Queremos retornar qualquer postagem em que qualquer um dos valores na postagem[categories] também estão presentes em currentFilters[categories].
Podemos fazer isso usando o .filter()
e .some
método.
o
some()
O método testa se pelo menos um elemento na matriz passa no teste implementado pela função fornecida. – MDN
Filtraremos a matriz de postagem para retornar qualquer postagem em que alguns dos valores em post.categories
estão incluídos em nosso currentFilters.categories
. Aqui está a aparência do código:
1 |
if (filters.categories.length > 0) { |
2 |
filteredPosts = filteredPosts.filter((post) => |
3 |
post.categories.some((category) => { |
4 |
return filters.categories.includes(category); |
5 |
})
|
6 |
);
|
7 |
}
|
Em seguida, passaremos os FilterPosts para o nosso filtro de nível também. É aqui que entra a lógica de filtro múltiplo, pois só realizamos a lógica de filtro no valor já filtrado.
Ao filtrar com uma string, só precisamos verificar se o post.level
valor está incluso em nosso currentFilters.level
variedade
1 |
if (filters.level.length > 0) { |
2 |
filteredPosts = filteredPosts.filter((post) => |
3 |
filters.level.includes(post.level) |
4 |
);
|
5 |
}
|
Para uma implementação de filtro mais avançada, podemos compactar nossas funções de filtro de categoria e filtro de nível em uma só.
1 |
let filterKeys = Object.keys(filters); |
2 |
|
3 |
filterKeys.forEach((key) => { |
4 |
let currentKey = filters[key] |
5 |
if (currentKey.length <= 0) { |
6 |
return; |
7 |
}
|
8 |
|
9 |
filteredPosts = filteredPosts.filter((post) => { |
10 |
let currentValue = post[key] |
11 |
return Array.isArray(currentValue) |
12 |
? currentValue.some((val) => currentKey.includes(val)) |
13 |
: currentKey.includes(currentValue); |
14 |
});
|
15 |
});
|
Na função acima, temos:
- Obtenha os valores-chave em nosso
currentFilters
objeto. Isso retornará uma matriz["categories", "level"]
- Mapa através do nosso
filterKeys
array e executar um retorno antecipado se ocurrentKey
é uma matriz vazia - Execute a função de filtro em nosso
filteredPosts
array dependendo se o valor do filtro atual é um array ou não.
Finalmente, podemos definir nossa lógica multifiltro:
- Criar uma função
handleFilterPosts()
que aceita um parâmetro de filtros - Definir
filteredPosts
por todos os valores em nosso array currentFilters. - Também atualizaremos nosso
postCount
enoResults
elementos com base nos dados emfilteredPosts
- Por fim, limparemos todos os elementos em posts-container e anexaremos o novo filterData ao contêiner.
1 |
const handleFilterPosts = (filters) => { |
2 |
let filteredPosts = [...postsData]; |
3 |
let filterKeys = Object.keys(filters); |
4 |
|
5 |
filterKeys.forEach((key) => { |
6 |
let currentKey = filters[key] |
7 |
if (currentKey.length <= 0) { |
8 |
return; |
9 |
}
|
10 |
|
11 |
filteredPosts = filteredPosts.filter((post) => { |
12 |
let currentValue = post[key] |
13 |
return Array.isArray(currentValue) |
14 |
? currentValue.some((val) => currentKey.includes(val)) |
15 |
: currentKey.includes(currentValue); |
16 |
});
|
17 |
});
|
18 |
|
19 |
postCount.innerText = filteredPosts.length; |
20 |
|
21 |
if (filteredPosts.length == 0) { |
22 |
noResults.innerText = "https://webdesign.tutsplus.com/tutorials/Sorry, we couldn't find any results."https://webdesign.tutsplus.com/tutorials/; |
23 |
} else { |
24 |
noResults.innerText = ""https://webdesign.tutsplus.com/tutorials/; |
25 |
}
|
26 |
|
27 |
postsContainer.innerHTML = ""https://webdesign.tutsplus.com/tutorials/; |
28 |
filteredPosts.map((post) => createPost(post)); |
29 |
};
|
Conclusão
E com isso, configuramos nossa implementação multifiltro!