Funkcja rekurencyjna to taka, która wywołuje samą siebie, co jest bardzo przydatne kiedy musimy przetwarzać dane o strukturze drzewa. Typowym przykładowym jest analiza zagnieżdżonych struktur JSON
, gdzie każdy element może zawierać inne elementy. Tego typu struktura często występuje w drzewach kategorii, drzewach plików itp.
Krok po kroku przedstawię pokażę, jak można zaimplementować funkcję rekurencyjną w JavaScript do przetwarzania zagnieżdżonych danych JSON i przedstawienia ich jako rozwijalne drzewo. Poniżej możesz zobaczyć demo rozwijanej listy.
Dane
Często dane do przetworzenia pochodzą z API. W moim przykładzie dla uproszczenia umieszczę dane w pliku o nazwie data.js
. Jego zawartość wygląda jak niżej
Przykładowe dane w pliku data.js
export const data = [
{
category: "Elektronika",
subcategories: [
{
category: "Komputery",
subcategories: [
{
category: "Laptopy",
subcategories: [],
},
{
category: "Komputery stacjonarne",
subcategories: [],
},
{
category: "Tablety",
subcategories: [
{
category: "8 cali",
subcategories: [],
objects: [
{ name: "Model 8-1" },
{ name: "Model 8-2" },
{ name: "Model 8-3" },
],
},
{
category: "10 cali",
subcategories: [],
objects: [
{ name: "Model 10-1" },
{ name: "Model 10-2" },
{ name: "Model 10-3" },
{ name: "Model 10-4" },
{ name: "Model 10-5" },
],
},
{
category: "12 cali",
subcategories: [],
},
],
objects: [
{ name: "Promocyjne" },
{ name: "Powystawowe" }
],
},
],
},
{
category: "Telefony",
subcategories: [
{
category: "Smartfony",
subcategories: [],
},
{
category: "Telefony stacjonarne",
subcategories: [],
},
],
},
{
category: "Telewizory",
subcategories: [
{
category: "Telewizory LED",
subcategories: [],
objects: [
{ name: "Samsung" },
{ name: "LG" },
{ name: "Sharp" }
],
},
{
category: "Telewizory OLED",
subcategories: [],
},
],
},
],
},
{
category: "Rowery",
subcategories: [],
},
];
Aby łatwiej zobrazować jak będzie wyglądać struktura danych koniecznie zobacz demo. Jak można zauważyć dane są zbudowane w ten sam sposób. Na każdym poziomie jest nazwa kategorii category
oraz pod kategorie subcategories
. Wewnątrz subcategories
mamy ten sam podział. Wewnątrz niektórych subcategories
mamy objects
, które będą czymś w rodzaju linków.
Nie ma żadnego znaczenia ile będzie subcategories
wewnątrz subcategories
oraz objects
. Ich ilość oraz ilość poziomów zagnieżdżenia, również nie ma znaczenia. Funkcja rekurencyjna przejdzie przez całe drzewo danych i wyświetli je w postaci rozwijanej listy.
Początkowe wyświetlanie danych
Pamiętaj, że prezentowane tutaj funkcje odnoszą się do konkretnego układu danych. Jeżeli Twoje dane wyglądają inaczej, prawdopodobnie będziesz musiał nie co zmienić funkcje. Jeżeli będziesz mieć trudności z dopasowaniem funkcji do swoich danych, zostaw wiadomość w komentarzu.
Na początku utwórzmy plik script.js
i dołączmy go do pliku index.html
.
Tworzenie pliku script,js
import { data } from "./data.js";
console.log(data);
W pliku index.html
<html>
<head>
<script type="module" defer src="script.js"></script>
</head>
<body>
<div class="js-category-tree"></div>
</body>
</html>
Zanim zaczniemy rekurencyjne wyświetlać dane, w elemencie js-category-tree
wyświetlimy elementy najwyższego poziomu. W pliku script.js
utwórz funkcję renderCategoryTree()
, która jako argument przyjmie dane i zwróci element <ul>
z <li>
. Natomiast w funkcji init()
utworzymy uchwyt do elementu <div>
o nazwie klasy js-category-tree
, w której wyświetlimy utworzony element <ul>
.
Utworzenie dwóch funkcji init() oraz renderCategoryTree()
import { data } from "./data.js";
const renderCategoryTree = (list) => {
const ul = document.createElement("ul");
list.forEach((item) => {
const li = document.createElement("li");
li.innerHTML = `<span>${item.category}</span>`;
li.classList.add("js-sublist");
ul.appendChild(li);
});
return ul;
}
const init = () => {
const rootElement = document.querySelector(".js-category-tree");
rootElement.appendChild(renderCategoryTree(data));
};
init();
Powyższy kod utworzy prostą listę z dwoma elementami jak niżej
- Elektronika
- Rowery
W funkcji renderCategoryTree()
, na początku tworzymy element <ul>
. W pętli forEach()
, tworzymy element <li>
, w którym w znaczniku <span>
umieszczamy nazwę kategorii, pole category
z danych. Do elementu <li>
dodajemy również klasę js-sublist
, którą wykorzystamy później do otwierania i zamykania listy. Funkcja appendChild()
dodaje utworzone <li>
do elementu <ul>
.
Natomiast w funkcji init()
, odwołujemy się do elementu HTML o nazwie klasy js-category-tree
i wyświetlamy w niej utworzoną listę.
Wywoływanie funkcji rekurencyjnie
Aby przejść przez zagnieżdżenia w strukturze danych musimy wywołać naszą funkcję rekurencyjnie. Jak wiadomo, funkcje wywołuje się poprzez podanie jej nazwy oraz podaniu argumentów jeżeli tego wymaga. Ten sam zabieg wykonamy w naszej funkcji, tylko jej wywołanie podamy wewnątrz funkcji, którą wywołujemy.
Do wcześniejszego kodu dodałem instrukcję warunkową if
sprawdzającą, czy dane zagnieżdżenie, posiada w sobie podkategorie, pole subcategories
z danych oraz czy dana tablica posiada elementy. Jeżeli tak wywołujemy funkcję. Dodaj również consol.log()
na samym początku funkcji, aby zobaczyć co kryje się pod argumentem funkcji list
.
Wywołanie funkcji rekurencyjnie
import { data } from "./data.js";
const renderCategoryTree = (list) => {
console.log(list);
const ul = document.createElement("ul");
list.forEach((item) => {
const li = document.createElement("li");
li.innerHTML = `<span>${item.category}</span>`;
li.classList.add("js-sublist");
ul.appendChild(li);
if (item.subcategories?.length) {
renderCategoryTree(item.subcategories);
}
});
return ul;
}
const init = () => {
const rootElement = document.querySelector(".js-category-tree");
rootElement.appendChild(renderCategoryTree(data));
};
init();
Stało się to czego oczekiwaliśmy. W konsoli zobaczymy wszystkie zagnieżdżania subcategories
, z każdego poziomu. Dodajmy je zatem do naszego drzewa. Aby to zrobić, musimy każde rekurencyjne wywołanie zapisać do zmiennej i dodać do elementu <ul>
, tak aby był budowany dalej a nie nadpisywany nowymi wartościami.
Wyświetlenie danych rekurencyjnie
import { data } from "./data.js";
const renderCategoryTree = (list) => {
const ul = document.createElement("ul");
list.forEach((item) => {
const li = document.createElement("li");
li.innerHTML = `<span>${item.category}</span>`;
li.classList.add("js-sublist");
ul.appendChild(li);
if (item.subcategories?.length) {
const subUl = renderCategoryTree(item.subcategories);
li.appendChild(subUl);
}
});
return ul;
}
const init = () => {
const rootElement = document.querySelector(".js-category-tree");
rootElement.appendChild(renderCategoryTree(data));
};
init();
Teraz w przeglądarce zobaczymy w pełni wyrenderowane drzewo zawierające subcategories. Na razie nie wyświetlamy elementów w objects
, gdyż się do nich nie dowołujemy. To zrobimy za chwilę. Nasze drzewo powinno wyglądać jak to poniżej.
- Elektronika
- Komputery
- Laptopy
- Komputery stacjonarne
- Tablety
- 8 cali
- 10cali
- 12 cali
- Telefony
- Smartfony
- Telefony stacjonarne
- Telewizory
- Telewizory LED
- Telewizory OLED
- Komputery
- Rowery
W konsoli w inspektorze zobacz jak obecnie jest zbudowane drzewo kategorii. Wszystko opiera się na liście <ul>
oraz <li>
, wewnątrz którego mamy kolejne listy <ul>
z elementami <li>
. I tak dalej. To sprawia, że nawigacja po elementach będzie prosta.
Wyświetlanie więcej danych
Na razie do tej pory wyświetlamy tylko subcategories
, czyli takie jak by foldery bez ich zawartości, które znajdują się w objects
. Trzeba zauważyć że subcategories
oraz objects
mogą być na tym samym poziomie danych.
Do obecnej funkcji dopiszmy kolejny warunek if
, w którym sprawdzimy czy objects
istnieje oraz czy posiada jakieś elementy. Jeżeli tak, utworzymy osobną listę <ul>
z <li>
, w której umieścimy dane z objects
. Nową utworzoną listę dodamy do <li>
, do którego należą elementy w objects
.
Wyświetlenie w drzewie elementów z objects.
import { data } from "./data.js";
const renderCategoryTree = (list) => {
const ul = document.createElement("ul");
list.forEach((item) => {
const li = document.createElement("li");
li.innerHTML = `<span>${item.category}</span>`;
li.classList.add("js-sublist");
ul.appendChild(li);
if (item.subcategories?.length) {
const subUl = renderCategoryTree(item.subcategories);
li.appendChild(subUl);
}
if (item.objects?.length) {
const ulObject = document.createElement("ul");
item.objects.forEach((subject, index) => {
const liObject = document.createElement("li");
liObject.innerHTML = `<a href="#" class="object">${index + 1}. ${subject.name}</a>`;
liObject.classList.add("js-object");
ulObject.appendChild(liObject);
li.appendChild(ulObject);
});
}
});
return ul;
}
const init = () => {
const rootElement = document.querySelector(".js-category-tree");
rootElement.appendChild(renderCategoryTree(data));
};
init();
Wewnątrz warunku if
, tworzymy listę <ul>
oraz elementy listy <li>
na podstawie danych z objects
. Do elementu <li> dodajemy również klasę js-object
, do jego późniejszego wykorzystania.
Sprawdzając nasze drzewo w konsoli w inspektorze zobaczysz, że w elemencie <li>Tablety</li>
, znajdują się dwie listy <ul>
. Pierwsza z nich to subcategories
, w których znajdują się elementy objects
. Natomiast druga lista <ul>
to już objects
, które należą do <li>Tablety</li>
.
Zwijanie i otwieranie drzewa
Dodajmy trochę interaktywności do naszego drzewa kategorii. Na początku musimy wszystkie nasze listy <ul>
(subcategories
oraz objects
) ukryć. Dodajmy do nich klasę hidden
. Utwórz plik style.css
utwórz w nim klasę .hidden
i przypisz właściwość display: none;
. Następnie plik style.css
dodaj do pliku index.html
, aby klasa zadziałała. Dodajmy również klasę .show, aby pokazać listę.
Plik style.css
.hidden {
display: none
}
.show {
display: block
}
Musimy również dopisać funkcję która będzie obsługiwać kliknięcia elementy listy o nazwie klasy js-sublist
. Za pomocą querySelectorAll()
odnajdziemy wszystkie elementy listy i przypiszemy do nich akcję click
zmieniającą klasę hidden
na show
. Oczywiście wystarczy tylko usunąć klasę hidden, aby lista się pojawiła. Jednak w moim przykładzie, podmieniam również ikonki folderu, z zamkniętoego na otwarty. Mając dwie klasy łatwiej o podmianę.
Dodanie funkcji otwierającej i ukrywającej podkategorie
import { data } from "./data.js";
const renderCategoryTree = (list) => {
const ul = document.createElement("ul");
list.forEach((item) => {
const li = document.createElement("li");
li.innerHTML = `<span>${item.category}</span>`;
li.classList.add("js-sublist");
ul.appendChild(li);
if (item.subcategories?.length) {
const subUl = renderCategoryTree(item.subcategories);
li.appendChild(subUl);
subUl.classList.add("hidden");
}
if (item.objects?.length) {
const ulObject = document.createElement("ul");
item.objects.forEach((subject, index) => {
const liObject = document.createElement("li");
liObject.innerHTML = `<a href="#" class="object">${index + 1}. ${subject.name}</a>`;
liObject.classList.add("js-object");
ulObject.appendChild(liObject);
li.appendChild(ulObject);
ulObject.classList.add("hidden");
});
}
});
return ul;
}
const toggleCategoryTree = () => {
const sublist = document.querySelectorAll(".js-sublist");
sublist.forEach((item) => {
item.addEventListener("click", (event) => {
event.stopPropagation();
const subListAll = item.querySelectorAll(":scope > ul);
subListAll.forEach((subItem) => {
if (subItem) {
subItem.classList.toggle("hidden");
subItem.classList.toggle("show");
}
});
});
});
};
const init = () => {
const rootElement = document.querySelector(".js-category-tree");
rootElement.appendChild(renderCategoryTree(data));
toggleCategoryTree();
};
init();
Zwróć uwagę, że w linii 39
ponownie używamy querySelectorAll()
, gdyż na tym samym poziomie mogą znajdować się subcategories
oraz objects
. Wówczas po kliknięciu musimy pokazać obie listy.
Należy również zadbać o wyświetlenie informacji kiedy subcategories
lub objects
jest puste lub nieistnieją. Wówczas należy dopisać warunek else
, w miejscu w którym dokonujemy rekurencji funkcji. Do <li>
dodamy znacznik <p>
z informacją o braku elementów.
Dodanie obsługi pustych elementów.
import { data } from "./data.js";
const renderCategoryTree = (list) => {
const ul = document.createElement("ul");
list.forEach((item) => {
const li = document.createElement("li");
li.innerHTML = `<span>${item.category}</span>`;
li.classList.add("js-sublist");
ul.appendChild(li);
if (item.subcategories?.length || item.objects?.length) {
const subUl = renderCategoryTree(item.subcategories);
li.appendChild(subUl);
subUl.classList.add("hidden");
} else {
const empty = document.createElement("p");
empty.innerText = "Brak elementów";
empty.classList.add("hidden", "empty");
li.appendChild(empty);
}
if (item.objects?.length) {
const ulObject = document.createElement("ul");
item.objects.forEach((subject, index) => {
const liObject = document.createElement("li");
liObject.innerHTML = `<a href="#" class="object">${index + 1}. ${subject.name}</a>`;
liObject.classList.add("js-object");
ulObject.appendChild(liObject);
li.appendChild(ulObject);
ulObject.classList.add("hidden");
});
}
});
return ul;
}
const toggleCategoryTree = () => {
const sublist = document.querySelectorAll(".js-sublist");
sublist.forEach((item) => {
item.addEventListener("click", (event) => {
event.stopPropagation();
const subListAll = item.querySelectorAll(":scope > ul);
subListAll.forEach((subItem) => {
if (subItem) {
subItem.classList.toggle("hidden");
subItem.classList.toggle("show");
}
});
});
});
};
const init = () => {
const rootElement = document.querySelector(".js-category-tree");
rootElement.appendChild(renderCategoryTree(data));
toggleCategoryTree();
};
init();
Akcja dla obiektu listy
Nasz lista nie do końca działa prawidłowo. Kiedy klikniemy w element objects
, nasz lista się zamyka. Aby temu zapobiec wystarczy dla wszystkich elementów o nazwie klasy js-object
, przypisać akcję click
i dodać event.stopPropagation()
, tak aby kliknięcie nie przechodziło przez wszystkie elementy.
Dopiszmy funkcję, która nie tylko będzie zapobiegać przechodzeniu kliknięcia, ale również wyświetli nad listą nazwę klikniętego elementu. W pliku index.html dodajmy element, w którym będziemy wyświetlać nazwę klikniętego elementu z objects
.
Plik index.html
<html>
<head>
<script type="module" defer src="script.js"></script>
</head>
<body>
<p>Wybrany element to: <span class="choice">Nic jeszcze nie wybrano</span></p>
<div class="js-category-tree"></div>
</body>
</html>
Teraz dodajmy funkcję obsługującą kliknięcie elementu objects
. W funkcji tej przypisujemy nazwę elementu do elementu o nazwie klasy choice
. Natomiast w funkcji init()
wywołamy naszą funkcję oraz uzyskamy dostęp do elementu choice
.
Dodanie akcji dla kliknięcia obiektu
import { data } from "./data.js";
const renderCategoryTree = (list) => {
const ul = document.createElement("ul");
list.forEach((item) => {
const li = document.createElement("li");
li.innerHTML = `<span>${item.category}</span>`;
li.classList.add("js-sublist");
ul.appendChild(li);
if (item.subcategories?.length || item.objects?.length) {
const subUl = renderCategoryTree(item.subcategories);
li.appendChild(subUl);
subUl.classList.add("hidden");
} else {
const empty = document.createElement("p");
empty.innerText = "Brak elementów";
empty.classList.add("hidden", "empty");
li.appendChild(empty);
}
if (item.objects?.length) {
const ulObject = document.createElement("ul");
item.objects.forEach((subject, index) => {
const liObject = document.createElement("li");
liObject.innerHTML = `<a href="#" class="object">${index + 1}. ${
subject.name
}</a>`;
liObject.classList.add("js-object");
ulObject.appendChild(liObject);
li.appendChild(ulObject);
ulObject.classList.add("hidden");
});
}
});
return ul;
};
const toggleCategoryTree = () => {
const sublist = document.querySelectorAll(".js-sublist");
sublist.forEach((item) => {
item.addEventListener("click", (event) => {
event.stopPropagation();
const subListAll = item.querySelectorAll(":scope > ul, :scope > p");
subListAll.forEach((subItem) => {
if (subItem) {
subItem.classList.toggle("hidden");
subItem.classList.toggle("show");
}
});
});
});
};
const clickObiect = (choice) => {
const objectslist = document.querySelectorAll(".js-object");
objectslist.forEach((item) => {
item.addEventListener("click", (event) => {
event.stopPropagation();
choice.innerText = item.textContent;
});
});
};
const init = () => {
const rootElement = document.querySelector(".js-category-tree");
const choiceElement = document.querySelector(".choice");
rootElement.appendChild(renderCategoryTree(data));
toggleCategoryTree();
clickObiect(choiceElement);
};
init();
Rekurencja w React
Rekurencja w React, będzie wyglądała bardzo podobnie jak w czystym JavaScript. Należy jedynie pamiętać, aby wywołać komponent renderujący listę wewnątrz samego komponentu. Poniżej bardzo uproszczony przykład.
Główny plik aplikacji App.jsx
// Zakładam komponentu RenderTree
// dataAPI - do dane pochodzące z API
export const App = () => {
return dataAPI.map((item, index) => {
<RenderTree key={index} dataTree={item} />;
});
};
Komponent RenderTree.jsx
export const RenderTree = ({ dataTree }) => {
return (
<>
<li>{dataTree.name}</li>
{dataTree.subcategories?.length && (
<ul>
{dataTree.subcategories.map((item, index) => {
<RenderTree key={index} dataTree={item} />;
})}
;
</ul>
)}
</>
);
};
Komponent renderuje elementy listy <li>
. Jednak kiedy dataTree
posiada podkategorie, wywołujemy ten sam komponent tworząc kolejną listę.
W praktyce
Wywołanie funkcji rekurencyjnie świetnie sprawdzi się wszędzie tam, gdzie trzeba wyświetlić dane w postaci rozwijanego drzewa. Układ plików i folderów, czy folderów i projektów. Można również dodać akcje przeciągania i upuszczania, aby zmieniać pozycje poszczególnych elementów. Tylko wtedy każdy element drzewa musi posiadać unikalny identyfikator, aby wiedzieć jaki element jest przeciągany i na jaki element został upuszczony. Wtedy można odpowiednio zmodyfikować dane i wyświetlić zaktualizowaną wersję użytkownikowi.
Podobał się artykuł?
Komentarze
Brak komentarzy. Bądź pierwszą osobą, która zostawi komentarz!