braesident / listjs
The nicest list js clone ever
Package info
Language:JavaScript
Type:extension
pkg:composer/braesident/listjs
Requires
None
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Kleine, framework-unabhängige Liste mit Suche, Filter, Sortierung, Pagination, progressivem Lazy Loading und optionaler Fuzzy-Suche. Direkt im Browser nutzbar, ohne Bundler oder module.exports.
Herkunft und Lizenz
Dieses Projekt basiert auf List.js von Jonny Strömberg (javve) und wird von Sebastian Braesicke als eigenständige Variante weiterentwickelt. Zu den Erweiterungen gehören unter anderem progressives Lazy Loading, zusätzliche Methoden zur Item-Verwaltung und Gruppierung mit Zwischenüberschriften.
Der ursprüngliche Code und die Weiterentwicklungen stehen unter der MIT-Lizenz. Die Copyright-Hinweise des Ursprungsprojekts und für die eigenen Änderungen sind in der Lizenzdatei enthalten.
Schnellstart
Einfach List.js einbinden und eine Container-Struktur bereitstellen:
<div id="users"> <input class="search" placeholder="Suche..." /> <button class="sort" data-sort="name">Sortiere nach Name</button> <ul class="list"> <li> <span class="name"></span> <span class="email"></span> </li> </ul> <ul class="pagination"></ul> </div> <script src="List.js"></script> <script> const options = { valueNames: [ 'name', 'email' ], page: 5, pagination: { paginationClass: 'pagination' } }; const users = new ListJS('users', options, [ { name: 'Ada', email: 'ada@example.com' }, { name: 'Linus', email: 'linus@example.com' } ]); </script>
Hinweis: In klassischen Script-Tags ist ListJS global verfügbar. Wenn du es explizit
am window oder als ES-Module exportieren willst, kannst du die auskommentierten
Zeilen am Ende von List.js aktivieren.
HTML-Struktur
- Container-Element mit
id(oder direkt als DOM-Element übergeben). - Kind-Element mit Klasse
listClass(default:list), darin Listeneinträge. - Optional: Eingabefeld mit Klasse
searchClass(default:search). - Optional: Sort-Buttons/Links mit Klasse
sortClass(default:sort) unddata-sort.
Optionen (Auszug)
Alle Optionen werden auf die Instanz gemerged. Wichtige Optionen:
listClass(default:list) – CSS-Klasse für die Liste.searchClass(default:search) – CSS-Klasse für Sucheingaben.sortClass(default:sort) – CSS-Klasse für Sort-Buttons.valueNames(default:[]) – Definiert, welche Werte in Items gebunden werden.item– Template für Items: String (HTML), DOM-Id oder Funktion(values) => html.page(default:10000) – Anzahl Items pro Seite.indexAsync(default:false) – Listenelemente asynchron indizieren.searchDelay(default:0) – Debounce in ms für Sucheingaben.searchColumns(default:undefined) – Spalten für Suche, falls nicht ausvalueNames.searchInfos(default:0) – Wenn truthy, fülltitem.matchingValues.sortFunction– Custom Sortierfunktion(a, b, options) => number.alphabet– Alphabet für natürliche Sortierung.pagination– Objekt oder Array von Objekten für Pagination-UI.lazyLoad(default:false) – Objekt für progressives Rendering (siehe unten); nicht mitpaginationkombinierbar.fuzzySearch– Optionen für die Fuzzy-Suche (siehe unten).iterationPlaceholder(default:_iterate) – Platzhalter für Attribute (siehe unten).iterationAttributes(default:[ 'id', 'for' ]) – Attribute mit Platzhalter.iterationStart(default:0) – Offset für Nummerierung.iterationFormatter– Funktion zur Erzeugung des Attributwerts.groupBy– Gruppierung mit Zwischenüberschriften (siehe unten).
valueNames Varianten
valueNames bestimmt, wie Werte auf das DOM-Template gemappt werden. Der Key im
Werteobjekt muss zum Eintrag in valueNames passen. valueNames kann Strings
oder Objekte enthalten:
valueNames: [ 'name', { data: [ 'id', 'role' ] }, { name: 'link', attr: 'href', prefix: 'mailto:' }, { class: 'title' [, all: true/false] }, { value: 'inputValue' }, { prop: 'checked', name: 'isActive' } ]
Details:
- String wie
'name'setztinnerHTMLdes ersten Elements mit Klasse.name. { class: 'title' }ist wie der String, mit{ all: true }werden alle.titlegesetzt.{ data: [ 'id', 'role' ] }setztdata-idunddata-roleam Item-Root.datakann auch Objekte enthalten, z. B.{ data: [ { name: 'isRead', fn: v => (v ? 1 : 0) } ] }.{ name: 'link', attr: 'href', prefix: 'mailto:' }setzthrefauf.linkund hängt optionalprefixvor den Wert.- Für CSS-Klassen ergänzen (statt überschreiben):
{ name: 'icon', attr: 'class', classAttr: true }auf z. B.<i class="icon"></i>. Dadurch bleiben bestehende Klassen erhalten und der neue Wert wird als zusätzliche Klasse(n) gesetzt. { value: 'inputValue' }setztelement.valueauf dem Element mit Klasse.inputValue. Mit{ target: 'my-input' }kannst du eine andere Klasse als Ziel angeben.{ prop: 'checked', name: 'isActive' }setzt eine DOM-Property (checked/disabled) auf dem Element mit Klasse.isActive.- Optional kannst du pro Eintrag eine Transform-Funktion angeben:
{ class: 'preview', fn: (value) => value.substring(0, 100) }. Alternativ:{ class: 'preview', fn: 'substring', params: [ 0, 100 ] }. Standard-Signatur istfn(value, item, list, valueName, ...).itemist dabei das ListJS-Item-Objekt. Wenn du das Item als ersten Parameter möchtest, nutzefnArg: 'item', z. B.:{ class: 'avatar-initials', fnArg: 'item', fn: (item) => CRM.tools.getInitials(item.cName) }. Hier istitemjetzt das Werteobjekt. Mitaltkannst du auf einen anderen Key fallbacken, falls der Wert leer ist (funktioniert auch ohnefn):{ class: 'preview', fn: 'substring', params: [ 0, 100 ], alt: 'content' }.altgreift auch dann, wenn der Ziel-Key nicht existiert - ideal für Derived-Felder:{ class: 'avatar-initials', alt: 'fromName' }. - Mit
concatkannst du Werte aus mehreren Keys zusammenbauen:{ class: 'title', concat: { list: [ 'name', 'zuname' ], as: 'title' } }. Mit Attributen z. B.:{ name: 'link', attr: 'href', prefix: 'mailto:', concat: { list: [ 'name', 'zuname' ], as: 'link' } }.listunterstützt Keys (auch verschachtelt wie'from.name'),separator/sep(default: Leerzeichen),asfür einen zusätzlichen Alias-Key,force: trueum auch vorhandene Zielwerte zu überschreiben. Beispiel mitfn+alt:{ class: 'avatar-initials', alt: 'fullname', fn: (v) => CRM.tools.getInitials(v), concat: { list: [ 'name', 'zuname' ], as: 'fullname' } }. - Verschachtelte Daten (z. B.
from.name) solltest du vor demadd()flatten, z. B.{ fromName: mail.from.name }und dannvalueNames: [ 'fromName' ]. reIndex()/get()lesen nurdata,attrundinnerHTML;value/propsind write-only beim Setzen.
Iterationen (Loops)
Mit loop kannst du Arrays im Item rendern. Dazu wird das erste Element mit der
Loop-Klasse als Template geklont und pro Array-Eintrag eingefügt.
<div class="comm-item-labels"> <span class="labels"></span> </div>
valueNames: [ { loop: 'labels', valueNames: [ 'identifier', { data: [ 'id' ] } ] } ]
{ labels: [ { identifier: 'A', id: 0 }, { identifier: 'B', id: 1 } ] }
Ergebnis:
<div class="comm-item-labels"> <span class="labels" data-id="0">A</span> <span class="labels" data-id="1">B</span> </div>
Auch Arrays aus Strings werden unterstützt:
{ labels: [ 'A', 'B' ] }
<div class="comm-item-labels"> <span class="labels">A</span> <span class="labels">B</span> </div>
Hinweise:
loopkann auch als eigene Optionloops/loopübergeben werden.- Wenn kein passendes Element gefunden wird, wird der Loop übersprungen.
fn,params,altundconcatfunktionieren auch in Loop-valueNames.
API (Public)
new ListJS(containerOrId, options = {}, values?)add(values, callback?)– Fügt Werte hinzu; optional async per Callback (Chunks).prepend(values)– Fügt ein Objekt oder Array am Anfang ein und erhält dabei die Reihenfolge des Arrays. Gibt die eingefügten Items als Array zurück, bei leerer Eingabeundefined.remove(valueName, value, options?)– Entfernt Items nach Wert.removeBy(valueName, value, options?)– Entfernt alle Treffer; gibt deren Anzahl zurück. Der sofortige Listenaufbau ist optional.updateBy(valueName, value, newValues = {}, options = {})– Ergänzt/überschreibt Werte des ersten Treffers; gibt das Item odernullzurück. Standardmäßig ohne vollständigen Listenaufbau.upsert(valueName, value, newValues = {}, options = {})– Aktualisiert den ersten Treffer oder fügt ein Item hinzu; gibt dieses Item zurück. Baut standardmäßig die Liste neu auf.get(valueName, value)– Gibt Items als Array zurück.size()– Anzahl Items.clear()– Entfernt alle Items aus DOM und Liste und leert auchvisibleItemsundmatchingItems.show(i, page)– Zeigt ab Indexi(1-based)pageItems.reIndex()– List-Items aus DOM neu einlesen.toJSON()– Werte aller Items als Array.search(str, columns?, customSearch?)– Suche (unterstützt "quoted phrases").filter(fn?)– Filterfunktion;undefinedsetzt Filter zurück.sort(valueName | event, options?)– Sortierung, auch per Click-Handler.update()– Rendert den aktuellen Zustand.loadMore(count?)– Rendert den nächsten Lazy-Load-Block und gibt die neu sichtbaren Items als Array zurück; ohne aktives Lazy Loading oder weitere Treffer[].resetLazyLoad(options?)– Setzt Lazy Loading auf die anfängliche Item-Anzahl zurück; gibt die Instanz zurück.destroy()– Trennt Lazy-Load-Observer, entfernt deren Scroll-/Resize-Listener und verwirft ausstehende Fallback-Prüfungen; gibt die Instanz zurück. Entfernt weder Items noch sonstige Such-/Sortier-Listener.on(event, callback)/off(event, callback)fuzzySearch(str, columns?)– Fuzzy-Suche (wenn aktiviert).reset.search()/reset.filter()– setzt nur Search/Filter-Flags zurück.
Items gezielt ändern und positionieren
updateBy, upsert und removeBy suchen wie get per losem Vergleich (==).
newValues wird mit den vorhandenen Werten zusammengeführt; nicht angegebene Felder bleiben erhalten.
Beim Einfügen durch upsert muss der Suchschlüssel auch in newValues stehen:
Er wird nicht automatisch aus valueName und value übernommen.
const users = new ListJS('users', { valueNames: [ 'name', 'email', { data: [ 'id' ] } ] }); users.prepend([ { id: 1, name: 'Ada', email: 'ada@example.com' }, { id: 2, name: 'Linus', email: 'linus@example.com' } ]); // Aktualisiert das einzelne Item ohne vollständigen Listenaufbau. users.updateBy('id', 1, { email: 'ada@new.example.com' }); // Fügt ein Item ein oder aktualisiert es und verschiebt es an den Anfang. users.upsert('id', 3, { id: 3, name: 'Grace', email: 'grace@example.com' }, { position: 'start' }); // Mehrere Änderungen sammeln, anschließend einmal rendern. users.updateBy('id', 1, { name: 'Ada Lovelace' }, { trigger: false }); users.removeBy('id', 2, { update: false, trigger: false }); users.update();
Optionen:
| Option | Verhalten |
|---|---|
update |
Vollständigen Listenaufbau ausführen. Default: false bei updateBy, true bei upsert und removeBy. |
trigger |
Ohne vollständigen Listenaufbau trotzdem updated auslösen (default: true). Bei removeBy nur, wenn etwas entfernt wurde. Mit Listenaufbau löst update() das Event unabhängig von dieser Option aus. |
notCreate |
Für updateBy/upsert: Mit true kein fehlendes DOM-Element vorzeitig erstellen. Bei Lazy Loading ist dies für noch nicht erstellte Items bzw. neue Upserts automatisch aktiv. Ein anschließendes update() erstellt sichtbare Items trotzdem. |
position |
Für updateBy/upsert: 'start' oder 'end' verschiebt das Item bzw. bestimmt seine Einfügeposition. |
index |
Für updateBy/upsert: Zielindex ab 0, begrenzt auf den gültigen Bereich. position hat Vorrang. |
prepend / append |
Für updateBy/upsert: true als Kurzform für die Position am Anfang bzw. Ende. |
Ohne vollständigen Listenaufbau werden die sichtbare Reihenfolge, Gruppierung sowie
visibleItems und matchingItems nicht neu aufgebaut. Positionsänderungen werden erst
beim nächsten update() sichtbar; removeBy entfernt betroffene DOM-Elemente sofort.
Aktive Such-/Filterbedingungen und Sortierungen werden durch diese Methoden oder
update() nicht erneut ausgeführt. Bei relevanten Wertänderungen search, filter
bzw. sort erneut aufrufen.
Events
Registriere Listener mit list.on(event, fn):
updatedsearchStart,searchCompletefilterStart,filterCompletesortStart,sortCompleteparseCompletelazyLoadStart,lazyLoadComplete,lazyLoadExhausted
Jeder Listener erhält die Instanz als erstes Argument. Lazy-Load-Events liefern zusätzlich ein Detailobjekt als zweites Argument (siehe unten).
Pagination
Pagination wird über options.pagination aktiviert:
const list = new ListJS('users', { valueNames: [ 'name' ], page: 10, pagination: { paginationClass: 'pagination', innerWindow: 2, left: 1, right: 1 } });
Die Pagination nutzt intern eine kleine ListJS-Instanz und erwartet ein Element mit
Klasse pagination im Container.
Progressives Lazy Loading
Lazy Loading begrenzt zunächst die Anzahl gerenderter Items und erweitert sie beim
Scrollen blockweise. Alle übergebenen Daten bleiben im Speicher für Suche, Filter,
Sortierung, get(), size() und toJSON() verfügbar. Es werden keine Daten automatisch
vom Server geladen; bereits gerenderte Items bleiben beim Weiterscrollen im DOM.
<div id="lazy-users"> <input class="search" placeholder="Suche..." /> <div class="scroll-area" style="max-height: 400px; overflow-y: auto;"> <ul class="list"></ul> </div> </div>
const lazyUsers = new ListJS('lazy-users', { valueNames: [ 'name', 'email', { data: [ 'id' ] } ], item: '<li><span class="name"></span> <span class="email"></span></li>', lazyLoad: { mode: 'progressive', initialItems: 50, itemsPerLoad: 50, thresholdItems: 10, scrollContainer: '.scroll-area' } }); // Hier die vollständigen, bereits geladenen Datensätze übergeben. lazyUsers.add([ { id: 1, name: 'Ada', email: 'ada@example.com' } ]); lazyUsers.on('lazyLoadComplete', (list, detail) => { console.log(`${detail.renderedItems} von ${detail.matchingItems} gerendert`); }); // Optional manuell, beispielsweise über einen Mehr-anzeigen-Button: lazyUsers.loadMore(); // itemsPerLoad weitere Items lazyUsers.loadMore(25); // bis zu 25 weitere Items
| Option | Default | Bedeutung |
|---|---|---|
mode |
'progressive' |
Einziger unterstützter Modus. |
initialItems |
50 |
Anfängliche Anzahl gerenderter Items. |
itemsPerLoad |
50 |
Anzahl zusätzlicher Items pro Block. |
thresholdItems |
10 |
Beobachtet das erste der letzten zehn sichtbaren Items; sobald es den Scrollbereich schneidet, wird erweitert. 0 beobachtet das letzte Item. |
scrollContainer |
automatisch | DOM-Element, CSS-Selektor, window oder 'window'. Ohne Angabe wird ab der Liste der nächste Vorfahr einschließlich der Liste mit vertikalem Scroll-Overflow verwendet, sonst das Fenster. |
lazyLoad: {} aktiviert die Standardwerte. false, null oder undefined
deaktivieren Lazy Loading; true ist ungültig. Ungültige Zahlen fallen auf die
Standardwerte zurück. Ein ungültiger oder nicht gefundener Scroll-Selektor führt zu
einem Fehler. Für die automatische Erkennung zählen overflow-y: auto, scroll
und overlay.
Lazy Loading darf nicht mit einer gesetzten pagination-Option kombiniert werden;
auch pagination: false oder null führt zum Fehler. Die Renderanzahl wird durch
Lazy Loading statt durch page bestimmt. show() ist daher kein Ersatz für
loadMore() oder resetLazyLoad().
Suche, Filter und Sortierung arbeiten auf allen Items und setzen die Renderanzahl
auf initialItems sowie die Scrollposition an den Anfang zurück. Das gilt auch beim
Zurücksetzen von Suche oder Filter über search('') bzw. filter().
Gruppierung bleibt nutzbar; Gruppenüberschriften zählen nicht zum Item-Limit.
Automatisches Erweitern nutzt IntersectionObserver, ersatzweise Scroll-/Resize-Events.
lazyUsers.resetLazyLoad(); // Anfangsanzahl rendern und zum Anfang scrollen lazyUsers.resetLazyLoad({ scroll: false }); // Scrollposition beibehalten lazyUsers.resetLazyLoad({ update: false, scroll: false }); lazyUsers.update(); // aufgeschobenen Neuaufbau ausführen // Beim Abbau der Ansicht Lazy-Load-Beobachtung beenden: lazyUsers.destroy();
Lazy-Load-Events
lazyLoadStart– vor dem Rendern eines zusätzlichen Blocks.lazyLoadComplete– nach dessen Rendering;detail.itemsenthält die neu sichtbaren Items.lazyLoadExhausted– beim Erreichen der beobachteten Schwelle, wenn alle lokalen Treffer sichtbar sind, oder bei entsprechendem manuellemloadMore(). Erfordert ein beobachtetes Item, wird also nicht für eine leere Liste ausgelöst. Mehrfache Meldungen für dieselbe aktive Schwelle werden unterdrückt; nach Verlassen und erneutem Erreichen oder einer Änderung der Liste kann das Event erneut auftreten.
Das Detailobjekt enthält reason ('manual' oder 'observer', auch beim
Scroll-Fallback), previousLimit, nextLimit, renderedItems, matchingItems,
remainingItems und items. Die drei Felder mit Item-Anzahlen sind Zahlen;
items ist bei Start und Erschöpfung ein leeres Array. Beim Start beschreibt
renderedItems noch den Zustand vor dem zusätzlichen Rendering.
lazyLoadExhausted bezieht sich nur auf die aktuell lokal verfügbaren Treffer und
kann als Signal für eigenes Nachladen dienen; neue Daten lassen sich mit add() ergänzen.
Fuzzy-Suche
Aktivierbar über fuzzySearch:
const list = new ListJS('users', { valueNames: [ 'name' ], fuzzySearch: { searchClass: 'fuzzy-search', threshold: 0.4, distance: 100, multiSearch: true } });
Ein Input mit Klasse fuzzy-search triggert dann list.fuzzySearch().
Gruppierung (Zwischenüberschriften)
Mit groupBy kannst du Items nach einem Feld gruppieren. Die Gruppen werden als
Überschriften zwischen den Items gerendert.
const list = new ListJS('inbox', { valueNames: [ 'subject', 'createdAt' ], groupBy: [{ name: 'createdAt', filter: { today: 'Heute', yesterday: 'Gestern', thisWeek: 'Diese Woche', thisMonth: 'Dieser Monat', thisYear: 'Dieses Jahr', lastYear: 'Letztes Jahr', older: 'Älter' }, headerClass: 'listjs-group', weekStartsOn: 1 }] }); list.sort('createdAt', { order: 'desc' });
Hinweise:
- Die Filter-Keys
today,yesterday,thisWeek,thisMonth,thisYear,lastYear,oldersind eingebaute Datumsgruppen. - Die Reihenfolge der Gruppen folgt der Reihenfolge im
filter-Objekt. - Standard-Header-Tag:
li(UL/OL),tr(TBODY/THEAD/TFOOT), sonstdiv. headerClasskann mehrere Klassen enthalten (String mit Leerzeichen oder Array).- Optional kannst du
headerals String, Funktion oder Template-ID setzen. {{label}}/{{key}}werden ersetzt (auch in Attributen). Bei Template-ID wird dieidim Clone entfernt, um doppelte IDs zu vermeiden.
Beispiel mit Tabellen-Header:
groupBy: [{ name: 'createdAt', filter: { today: 'Heute', older: 'Älter' }, header: ({ label }) => `<tr class="listjs-group"><td colspan="5">${label}</td></tr>` }]
Header-Template per ID + Klassen-Mapping
Wenn {{label}} wegen SSR (z.B. Smarty) kollidiert, kannst du stattdessen Klassen nutzen:
<div id="group-header-template" class="listjs-group"> <span class="title label"></span> <span class="meta key" data-key=""></span> </div>
groupBy: [{ name: 'createdAt', header: 'group-header-template', labelClass: 'label', // Standard: "label" keyClass: 'key' // Standard: "key" (setzt data-key) }]
Iteration Placeholder (IDs/for)
Wenn Attribute in den Templates den Platzhalter enthalten, wird er beim Rendern automatisch ersetzt:
<label for="user-_iterate">User</label> <input id="user-_iterate" class="name" />
Mit iterationStart und iterationFormatter kannst du die Nummerierung steuern.
Hinweise
searchColumnsbestimmt die Spalten für die Suche; ohne Angabe wird ausvalueNamesabgeleitet (falls Items existieren).searchInfosbefülltitem.matchingValuesmit Trefferdetails.indexAsyncundadd(..., callback)nutzen Chunking, um große Listen flüssig zu halten.