Dynamische Baum-Struktur mit interaktiver Suche
Dieser Artikel zeigt, wie sich ein Baum, z.B. ein Verzeichnisbaum wie man ihn aus dem Explorer kennt, mittels Vue.js darstellen lässt. Das ganze versehen wir dann noch mit einer interaktiven Suchen, die die Elemente des Baumes durchsucht und filtert. Fertig sieht es dann so aus:
Dynamik dank Vue.js
Vue.js ist ein modernes JavaScript-Framework, mit dem sich dynamische Webanwendungen einfach und schnell erstellen lässt. Es ist ideal für neue Web-Projekte, z.B. wenn man eine moderne Plattform für Kryptowährungen wie Beispielsweise bitcoinsystem.app/de erstellt. Vue.js ist aber auch perfekt für bestehende Web-Projekte die ggf. noch auf reiner HTML- und PHP-Basis erstellt wurden. Einzelne Komponenten können in solchen Webanwendungen mit relativ wenig Aufwand dynamisch gemacht werden, um so einen höheren Komfort für eure Besucher zu bieten.
Dieser Artikel verwendet etwas weiterführende Konzepte von Vue.js. Für eine allgemeine Einführung in Vue.js empfehle ich: Vue.js Introduction
Rekursiver Baum inklusive Suche
Vorab der vollständige Code für die obige Demo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Rekursiver Baum inklusive Suche</title> <!-- Vue.js --> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <!-- Bootstrap --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"> <!-- Font Awesome --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css"> <style> .clickable { cursor: pointer; } </style> </head> <body> <div id="app" class="container" style="width: 500px; margin-top: 20px;"> <input v-model="query" /> <tree-menu v-for="node in baum" :node="node"></tree-menu> </div> <script> Vue.component('tree-menu', { props: ['node'], template: ` <ul class="tree-menu fa-ul" > <li v-if="node.shown" class="clickable" @click="node.open = !node.open"><span v-if="node.nodes.length > 0" class="fa-li"> <i v-bind:class="[node.open ? 'fas fa-angle-down' : 'fas fa-angle-right']"></i> </span> {{ node.name }} </li> <tree-menu v-if="node.open" v-for="child in node.nodes" :node="child" > </tree-menu> </ul> ` }); var app = new Vue({ el: '#app', data: { baum: [ {name: 'Tiere', shown: true, open: false, nodes: [ {name: 'Hund', shown: true, nodes: []}, {name: 'Katze', shown: true, nodes: []}, ]}, {name: 'Pflanzen', shown: true, open: false, nodes: [ {name: 'Bäume', shown: true, open: false, nodes: [ {name: 'Kiefer', shown: true, open: false, nodes: []}, {name: 'Fichte', shown: true, open: false, nodes: []}, {name: 'Tanne', shown: true, open: false, nodes: []} ]}, ]}, { name: 'Autos', shown: true, open: false, nodes: []} ], query: "" }, watch: { // query: function (query) { this.applyFilter(query); } }, methods: { //Wendet den query auf alle Elemente des Baums an applyFilter(query) { query = query.toLowerCase().trim(); if(query.length > 0) { for(let child of this.baum) { this.containsText(child, query); } } else { for(let child of this.baum) { child.open = false; this.showAll(child) } } }, //Setzt alle Elemente im node und dessen Kinder auf show=true showAll(node) { node.shown = true; for(let child of node.nodes) { this.showAll(child) } }, //Überprüft ob die Node oder eins der Kinder den Text enthält containsText(node, query) { let parent_match = node.name.toLowerCase().includes(query) let child_match = false; for(let child of node.nodes) { child_match = this.containsText(child, query) || child_match } node.open = child_match; node.shown = child_match || parent_match; if(parent_match) { this.showAll(node) } return child_match || parent_match; } } }); </script> </body> </html> |
Rekursive Darstellung des Baums
Als erstes definieren wir einen rekursiven Baum. Rekursion bedeutet in diesem Fall, dass jedes Element weitere Knoten (nodes) enthalten kann, die die entsprechenden Kind-Elemente sind.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
data: { baum: [ {name: 'Tiere', shown: true, open: false, nodes: [ {name: 'Hund', shown: true, nodes: []}, {name: 'Katze', shown: true, nodes: []}, ]}, {name: 'Pflanzen', shown: true, open: false, nodes: [ {name: 'Bäume', shown: true, open: false, nodes: [ {name: 'Kiefer', shown: true, open: false, nodes: []}, {name: 'Fichte', shown: true, open: false, nodes: []}, {name: 'Tanne', shown: true, open: false, nodes: []} ]}, ]}, { name: 'Autos', shown: true, open: false, nodes: []} ], query: "" }, |
Hier hat z.B. das Eltern-Element zu Pflanzen ein Kind-Element Bäume. Bäume hat wiederum drei Kinder: Kiefer, Fichte, und Tanne. Dies kann beliebig tief verwurzelt sein.
Neben den Namen und den Kind-Elementen (nodes), speichern wir ebenfalls Status-Flags (True/False) dazu ob ein Eintrag angezeigt werden soll (shown) und ob der Eintrag geöffnet oder geschlossen ist (open).
Da wir eine beliebig tiefen Baum haben können, ist die einfache Darstellung per Schleife nicht möglich. Stattdessen müssen wir auf rekursive Programmierung zurück greifen, um die Darstellung und gewisse Methoden umzusetzen.
Für die Darstellung des Baums nutzen müssen wir zuerst eine neue Componente definieren:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
Vue.component('tree-menu', { props: ['node'], template: ` <ul class="tree-menu fa-ul" > <li v-if="node.shown" class="clickable" @click="node.open = !node.open"><span v-if="node.nodes.length > 0" class="fa-li"> <i v-bind:class="[node.open ? 'fas fa-angle-down' : 'fas fa-angle-right']"></i> </span> {{ node.name }} </li> <tree-menu v-if="node.open" v-for="child in node.nodes" :node="child" > </tree-menu> </ul> ` }); |
Diese Komponente zeigt den Knoten an (node.name) und sofern node.open true ist, wird die Komponente tree-menu erneut aufgerufen um die Kinder-Elemente anzuzeigen.
Das ganze haben wir dann noch mit ein wenig Logik verstehen zur Steuerung des Pfeils, so dass dieser nur angezeigt wird falls Kinder vorhanden sind und für dieser dreht sich entsprechend, je nach Wert von node.open.
Um mehr über rekursive Komponenten in Vue.js zu erfahren, empfehle ich: Build A Collapsible Tree Menu With Vue.js Recursive Components und Recursive Components.
Interaktive Suche
Wie in dem Video oben gezeigt, ist noch eine Suchfunktion vorhanden die alle Elemente des Baums durchsuchen und nur entsprechende Übereinstimmungen anzeigt.
Um dies zu erzielen, definieren wir zuerst einen watch der die Query-Variable überwacht:
1 2 3 4 5 |
watch: { query: function (query) { this.applyFilter(query); } }, |
Sobald der Wert sich ändert, wird die Methode applyFilter aufgerufen.
Die Such-Logik sieht wie folgt aus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
applyFilter(query) { query = query.toLowerCase().trim(); if(query.length > 0) { for(let child of this.baum) { this.containsText(child, query); } } else { for(let child of this.baum) { child.open = false; this.showAll(child) } } }, //Setzt alle Elemente im node und dessen Kinder auf show=true showAll(node) { node.shown = true; for(let child of node.nodes) { this.showAll(child) } }, //Überprüft ob die Node oder eins der Kinder den Text enthält containsText(node, query) { let parent_match = node.name.toLowerCase().includes(query) let child_match = false; for(let child of node.nodes) { child_match = this.containsText(child, query) || child_match } node.open = child_match; node.shown = child_match || parent_match; if(parent_match) { this.showAll(node) } return child_match || parent_match; } |
Die Methode applyFilter prüft, ob etwas in die Suche eingegeben wurde. Falls diese Leer ist, setzt sie den Baum zurück indem alle Element auf shown=true gesetzt werden.
Sofern ein Such-Query vorhanden ist, wird die Methode this.containsText(child, query); auf jedes Hauptelement des Baums angewendet.
Diese Methode prüft, ob der Suchstring zum Namen des Eltern-Knoten passt (parent_match). Ebenfalls überprüft es, ob es zu einem der Kinder passt. child_match ist true, falls die Methode containsText für eins der Kinder true ist. Hier wird die angesprochene Rekursion verwendet. Sofern eins der Kind zu dem Suchstring passt, wird open auf true gesetzt damit der Treffer im Baum direkt angezeigt wird.
Sofern der Eltern-Knoten zum Suchstring passt, setzen wir alle Kinder-Elemente noch auf sichtbar (this.showAll(node)). So lassen sich alle Kinder-Elemente sehen, wenn der Such-String zum Eltern-Knoten passt. Ansonsten wären sie ausgeblendet und der Nutzer könnte sie nicht öffnen.
Autor: Nils Reimers