Die Ermittlung des effizientesten Wegs zur Verwaltung von Zuständen kann eine Herausforderung in CSS darstellen, aber glücklicherweise gibt es viele OOCSS-basierte Methodologien, die gute Lösungen bieten.
Artikelserie
- Verwaltung von Zuständen in CSS mit wiederverwendbaren JavaScript-Funktionen (Sie sind hier!)
- Aufgreifen der Ideen in diesem Artikel
Mein Favorit stammt von SMACSS (Scalable and modular architecture for CSS) und beinhaltet zustandsbezogene Klassen. Um die eigene Dokumentation von SMACSS zu zitieren, sind zustandsbezogene Klassen
Ein Zustand ist etwas, das alle anderen Stile ergänzt und überschreibt. Zum Beispiel kann ein Akkordeonabschnitt in einem eingeklappten oder ausgeklappten Zustand sein. Eine Nachricht kann einen Erfolgs- oder Fehlerzustand haben.
Zustände werden im Allgemeinen auf dasselbe Element wie eine Layoutregel oder auf dasselbe Element wie eine Basismodulklasse angewendet.
Eine meiner am häufigsten verwendeten zustandsbezogenen Klassen ist is-active. Am Beispiel des Akkordeons aus dem vorherigen Zitat würde is-active in diesem Fall alle erforderlichen CSS-Stile anwenden, um einen erweiterten Zustand darzustellen. Wie im folgenden Beispiel zu sehen ist
Siehe den Stift #1) Akkordeon-Komponente mit zustandsbezogener Klasse von Luke Harrison (@lukedidit) auf CodePen.
Sie werden feststellen, dass es etwas JavaScript gibt, das die Klasse is-active beim Erkennen eines Klickereignisses auf der Komponente umschaltet
var accordion = document.querySelectorAll(".c-accordion");
for(var i = 0; i < accordion.length; i++) {
var accordionHeader = accordion[i].querySelector(".c-accordion__header"),
accordionCurrent = accordion[i];
accordionHeader.addEventListener("click", function(){
accordionCurrent.classList.toggle("is-active");
});
}
Obwohl es sich um gültiges JavaScript handelt, müsste dies für jede andere Komponente, die die zustandsbezogene Klasse is-active über ein Klickereignis nutzt, immer wieder wiederholt werden, was zu vielen Duplikaten desselben Code-Snippets führt.
Nicht sehr effizient und definitiv nicht sehr DRY.
Ein besserer Ansatz wäre stattdessen, eine einzige Funktion zu schreiben, die dieselbe Aufgabe erfüllt und immer wieder mit verschiedenen Komponenten wiederverwendet werden kann. Machen wir das.
Erstellen einer einfachen wiederverwendbaren Funktion
Beginnen wir mit dem Erstellen einer einfachen Funktion, die ein Element als Parameter akzeptiert und is-active umschaltet
var makeActive = function(elem){
elem.classList.toggle("is-active");
}
Das funktioniert gut, aber wenn wir es in unser Akkordeon-JavaScript einfügen, gibt es ein Problem
var accordion = document.querySelectorAll(".c-accordion"),
makeActive = function(elem){
elem.classList.toggle("is-active");
}
for(var i = 0; i < accordion.length; i++) {
var accordionHeader = accordion[i].querySelector(".c-accordion__header"),
accordionCurrent = accordion[i];
accordionHeader.addEventListener("click", function(){
makeActive(accordionCurrent);
});
}
Obwohl die Funktion makeActive wiederverwendbar ist, müssen wir immer noch zuerst Code schreiben, um unsere Komponente und ihre inneren Elemente abzurufen. Es gibt also sicherlich viel Raum für Verbesserungen.
Um diese Verbesserungen vorzunehmen, können wir HTML5 benutzerdefinierte Datenattribute nutzen
<div class="c-accordion js-accordion">
<div class="c-accordion__header" data-active="js-accordion">My Accordion Component</div>
<div class="c-accordion__content-wrapper">
<div class="c-accordion__content">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce laoreet ultricies risus, sit amet congue nulla mollis et. Suspendisse bibendum eros sed sem facilisis ornare. Donec sit amet erat vel dui semper pretium facilisis eget nisi. Fusce consectetur vehicula libero vitae faucibus. Nullam sed orci leo. Fusce dapibus est velit, at maximus turpis iaculis in. Pellentesque ultricies ultrices nisl, eu consequat est molestie sit amet. Phasellus laoreet magna felis, ut vulputate justo tempor eu. Nam commodo aliquam vulputate.
</div>
</div>
</div>
Ein data-active-Attribut wurde dem Element hinzugefügt, das zuvor das is-active-Umschalten beim Klicken ausgelöst hat. Der Wert dieses Attributs repräsentiert das Element, auf dem das is-active-Umschalten stattfinden soll, was wie zuvor das übergeordnete c-accordion-Element ist. Beachten Sie die Hinzufügung einer neuen Klasse js-accordion anstelle des Hookings in die bestehende Klasse c-accordion. Dies dient dazu, funktionale Aspekte der Komponente von ihrer Gestaltung zu entkoppeln.
Werfen wir einen Blick auf das JavaScript
// Grab all elements with data-active attribute
var elems = document.querySelectorAll("[data-active]");
// Loop through if any are found
for(var i = 0; i < elems.length; i++){
// Add event listeners to each one
elems[i].addEventListener("click", function(e){
// Prevent default action of element
e.preventDefault();
// Grab linked elements
var linkedElement = document.querySelectorAll("." + this.getAttribute("data-active"));
// Toggle linked element if present
for(var i = 0; i < linkedElement.length; i++) {
linkedElement[i].classList.toggle("is-active");
}
});
}
Dies hat die Dinge sicherlich verbessert, da wir keinen Code mehr schreiben müssen, um Elemente abzurufen, sondern nur ein data-active-Attribut an unser Trigger-Element anhängen und ein Ziel-Element angeben müssen. Derzeit kann diese Funktion für jede andere Komponente verwendet werden, bei der eine klickbasierte is-active-Klasse ohne zusätzlichen Code erforderlich ist. Vollständiges Beispiel unten
Siehe den Stift #2) Akkordeon-Komponente mit wiederverwendbarer is-active Funktion von Luke Harrison (@lukedidit) auf CodePen.
Verbesserung unserer wiederverwendbaren Funktion
Diese wiederverwendbare Funktion funktioniert, aber wenn sie skaliert wird, müssen wir darauf achten, dass sich die Klassen von Trigger- und Zielelementen nicht gegenseitig beeinflussen. Im folgenden Beispiel würde das Klicken auf ein Akkordeon is-active auf allen auslösen.
<div class="c-accordion js-accordion">
<div class="c-accordion__header" data-active="js-accordion">First Accordion</div>
[...]
</div>
<div class="c-accordion js-accordion">
<div class="c-accordion__header" data-active="js-accordion">Second Accordion</div>
[...]
</div>
<div class="c-accordion js-accordion">
<div class="c-accordion__header" data-active="js-accordion">Third Accordion</div>
[...]
</div>
Das Hinzufügen von Nummern-Suffixen zu jeder js-accordion-Referenz löst das Problem, aber es ist eine Mühe, die wir vermeiden können. Eine gute Lösung wäre, stattdessen die Kapselung in unserer wiederverwendbaren Funktion zu implementieren, die es uns ermöglicht, unsere Toggles zu kapseln, sodass sie nur die gewünschten Elemente beeinflussen.
Um die Kapselung zu implementieren, müssen wir ein separates benutzerdefiniertes Attribut namens data-active-scope erstellen. Sein Wert sollte das übergeordnete Element darstellen, innerhalb dessen das Umschalten gekapselt werden soll, was in diesem Fall das übergeordnete js-accordion-Element ist.
<div class="c-accordion js-accordion">
<div class="c-accordion__header" data-active="js-accordion" data-active-scope="js-accordion">First Accordion</div>
[...]
</div>
<div class="c-accordion js-accordion">
<div class="c-accordion__header" data-active="js-accordion">Second Accordion</div>
[...]
</div>
Unter Verwendung des obigen HTML sollte das folgende Verhalten eintreten
- Wenn Sie das erste Akkordeon anklicken, weil es einen Bereich
js-accordionhat, werden nurdata-active-Elemente, die mit dieserjs-accordion-Instanz übereinstimmen oder deren Kinder sind,is-activeumschalten. - Wenn Sie das zweite Akkordeon anklicken, das keinen Bereich hat, wird
is-activeauf allen Instanzen vonjs-accordionumgeschaltet.
Vorausgesetzt, data-active-scope ist korrekt gesetzt, sollten alle Klassenumschaltungen innerhalb jedes js-accordion-Elements gekapselt werden, unabhängig von widersprüchlichen Klassennamen.
Hier ist das modifizierte JavaScript und ein funktionierendes Beispiel, das Akkordeons mit und ohne data-active-scope-Attribut zeigt
// Grab all elements with data-active attribute
var elems = document.querySelectorAll("[data-active]"),
// closestParent helper function
closestParent = function(child, match) {
if (!child || child == document) {
return null;
}
if (child.classList.contains(match) || child.nodeName.toLowerCase() == match) {
return child;
}
else {
return closestParent(child.parentNode, match);
}
}
// Loop through if any are found
for(var i = 0; i < elems.length; i++){
// Add event listeners to each one
elems[i].addEventListener("click", function(e){
// Prevent default action of element
e.preventDefault();
// Grab scope if defined
if(this.getAttribute("data-active-scope")) {
var scopeElement = closestParent(this, this.getAttribute("data-active-scope"));
}
if(scopeElement) {
// Grab scoped linked element
var linkedElement = scopeElement.querySelectorAll("." + this.getAttribute("data-active"));
// Convert to array
linkedElement = Array.prototype.slice.call(linkedElement);
// Check if our scope matches our target element and add to array if true.
// This is to make sure everything works when data-active matches data-active-scope.
if(scopeElement.classList.contains(this.getAttribute("data-active"))) {
linkedElement.unshift(scopeElement);
}
}
else {
// Grab linked element
var linkedElement = document.querySelectorAll("." + this.getAttribute("data-active"));
}
// Toggle linked element if present
for(var i = 0; i < linkedElement.length; i++) {
linkedElement[i].classList.toggle("is-active");
}
});
}
Siehe den Stift #3) Akkordeon-Komponente mit verbesserter wiederverwendbarer is-active Funktion von Luke Harrison (@lukedidit) auf CodePen.
Mehr als nur is-active
Unsere wiederverwendbare Funktion funktioniert jetzt gut und ist eine effiziente Methode, um is-active-Umschaltungen auf allen Arten von Komponenten einzurichten. Was aber, wenn wir eine ähnliche Umschaltung für eine andere zustandsbezogene Klasse einrichten müssen? Derzeit müssten wir die Funktion duplizieren und alle Verweise auf is-active auf die neue zustandsbezogene Klasse ändern. Nicht sehr effizient.
Wir sollten unsere wiederverwendbare Funktion verbessern, um jede Klasse durch Refactoring unserer Datenattribute zu akzeptieren. Anstatt das data-active-Attribut an unser Trigger-Element anzuhängen, ersetzen wir es durch Folgendes
data-class– Die Klasse, die wir hinzufügen möchten.data-class-element– Das Element, zu dem wir die Klasse hinzufügen möchten.data-class-scope– Das Bereichsattribut erfüllt dieselbe Funktion, wurde aber zur Konsistenz umbenannt.
Dies erfordert einige geringfügige Anpassungen an unserem JavaScript
// Grab all elements with data-active attribute
var elems = document.querySelectorAll("[data-class][data-class-element]");
// closestParent helper function
closestParent = function(child, match) {
if (!child || child == document) {
return null;
}
if (child.classList.contains(match) || child.nodeName.toLowerCase() == match) {
return child;
}
else {
return closestParent(child.parentNode, match);
}
}
// Loop through if any are found
for(var i = 0; i < elems.length; i++){
// Add event listeners to each one
elems[i].addEventListener("click", function(e){
// Prevent default action of element
e.preventDefault();
// Grab scope if defined
if(this.getAttribute("data-class-scope")) {
var scopeElement = closestParent(this, this.getAttribute("data-class-scope"));
}
if(scopeElement) {
// Grab scoped linked element
var linkedElement = scopeElement.querySelectorAll("." + this.getAttribute("data-class-element"));
// Convert to array
linkedElement = Array.prototype.slice.call(linkedElement);
// Check if our scope matches our target element and add to array if true.
// This is to make sure everything works when data-active matches data-active-scope.
if(scopeElement.classList.contains(this.getAttribute("data-class-element"))) {
linkedElement.unshift(scopeElement);
}
}
else {
// Grab linked element
var linkedElement = document.querySelectorAll("." + this.getAttribute("data-class-element"));
}
// Toggle linked element if present
for(var i = 0; i < linkedElement.length; i++) {
linkedElement[i].classList.toggle(this.getAttribute("data-class"));
}
});
}
Es würde im HTML wie folgt eingerichtet werden
<button class="c-button" data-class="is-loading" data-class-element="js-form-area">Submit</button>
Im folgenden Beispiel schaltet das Anklicken der c-button-Komponente die Klasse is-loading auf der js-form-area-Komponente um
Siehe den Stift #4) Formular-Komponente mit verbesserter wiederverwendbarer Funktion für jede Klasse von Luke Harrison (@lukedidit) auf CodePen.
Behandlung mehrerer Umschaltungen
Somit haben wir eine wiederverwendbare Funktion, die jede Klasse auf jedem Element umschaltet. Diese Klickereignisse können ohne zusätzlichen JavaScript-Code durch die Verwendung benutzerdefinierter Datenattribute eingerichtet werden. Es gibt jedoch noch Möglichkeiten, diese wiederverwendbare Funktion noch nützlicher zu machen.
Zurück zu unserem vorherigen Beispiel der Login-Formular-Komponente, was ist, wenn beim Klicken auf das c-button-Element zusätzlich zum Umschalten von is-loading auf js-form-area auch is-disabled auf allen Instanzen von c-input umgeschaltet werden soll? Im Moment ist das nicht möglich, da unsere benutzerdefinierten Attribute nur einen einzigen Wert pro Stück akzeptieren.
Lassen Sie uns unsere Funktion modifizieren, sodass sie anstelle jedes benutzerdefinierten Datenattributs, das nur einen einzelnen Wert akzeptiert, eine durch Kommas getrennte Liste von Werten akzeptiert – wobei jeder Wert in data-class mit dem Wert eines übereinstimmenden Index in data-class-element und data-class-scope verknüpft wird.
So zum Beispiel
<button class="c-button" data-class="is-loading, is-disabled" data-class-element="js-form-area, js-input" data-class-scope="false, js-form-area">Submit</button>
Unter Annahme der obigen Verwendung würde Folgendes geschehen, sobald c-button angeklickt wird
is-loadingwird aufjs-form-areaumgeschaltet.is-disabledwird aufjs-inputumgeschaltet und innerhalb des übergeordnetenjs-form-area-Elements gekapselt.
Dies erfordert weitere Änderungen an unserem JavaScript
// Grab all elements with data-active attribute
var elems = document.querySelectorAll("[data-class][data-class-element]");
// closestParent helper function
closestParent = function(child, match) {
if (!child || child == document) {
return null;
}
if (child.classList.contains(match) || child.nodeName.toLowerCase() == match) {
return child;
}
else {
return closestParent(child.parentNode, match);
}
}
// Loop through if any are found
for(var i = 0; i < elems.length; i++){
// Add event listeners to each one
elems[i].addEventListener("click", function(e){
// Prevent default action of element
e.preventDefault();
// Grab classes list and convert to array
var dataClass = this.getAttribute('data-class');
dataClass = dataClass.split(", ");
// Grab linked elements list and convert to array
var dataClassElement = this.getAttribute('data-class-element');
dataClassElement = dataClassElement.split(", ");
// Grab data-scope list if present and convert to array
if(this.getAttribute("data-class-scope")) {
var dataClassScope = this.getAttribute("data-class-scope");
dataClassScope = dataClassScope.split(", ");
}
// Loop through all our dataClassElement items
for(var b = 0; b < dataClassElement.length; b++) {
// Grab elem references, apply scope if found
if(dataClassScope && dataClassScope[b] !== "false") {
// Grab parent
var elemParent = closestParent(this, dataClassScope[b]),
// Grab all matching child elements of parent
elemRef = elemParent.querySelectorAll("." + dataClassElement[b]);
// Convert to array
elemRef = Array.prototype.slice.call(elemRef);
// Add parent if it matches the data-class-element and fits within scope
if(dataClassScope[b] === dataClassElement[b] && elemParent.classList.contains(dataClassElement[b])) {
elemRef.unshift(elemParent);
}
}
else {
var elemRef = document.querySelectorAll("." + dataClassElement[b]);
}
// Grab class we will add
var elemClass = dataClass[b];
// Do
for(var c = 0; c < elemRef.length; c++) {
elemRef[c].classList.toggle(elemClass);
}
}
});
}
Und hier ist ein weiteres funktionierendes Beispiel
Siehe den Stift #5) Formular-Komponente mit verbesserter wiederverwendbarer Funktion für mehrere Klassen von Luke Harrison (@lukedidit) auf CodePen.
Mehr als nur umschalten
Unsere wiederverwendbare Funktion ist jetzt ziemlich nützlich, aber sie geht davon aus, dass das Umschalten von Klassen das gewünschte Verhalten ist. Was ist, wenn wir möchten, dass der Auslöser beim Klicken eine Klasse entfernt, wenn sie vorhanden ist, und ansonsten nichts tut? Derzeit ist das nicht möglich.
Um die Funktion abzurunden, integrieren wir ein paar zusätzliche Logiken, um dieses Verhalten zu ermöglichen. Wir führen ein optionales Datenattribut namens data-class-behaviour ein, das folgende Optionen akzeptiert
toggle– Schaltetdata-classaufdata-class-elementum. Dies sollte auch das Standardverhalten sein, das eintritt, wenndata-class-behaviournicht definiert ist.add– Fügtdata-classaufdata-class-elementhinzu, wenn es nicht bereits vorhanden ist. Wenn es vorhanden ist, passiert nichts.remove– Entferntdata-classvondata-class-element, wenn es bereits vorhanden ist. Wenn es nicht vorhanden ist, passiert nichts.
Wie bei früheren Datenattributen ist dieses neue optionale Attribut eine durch Kommas getrennte Liste, um für jede Aktion unterschiedliche Verhaltensweisen zu ermöglichen. Wie hier
<button class="c-button" data-class="is-loading, is-disabled" data-class-element="js-form-area, js-input" data-class-behaviour="toggle, remove">Submit</button>
Unter Annahme des obigen HTML-Codes würde Folgendes geschehen, sobald c-button angeklickt wird
is-loadingwird aufjs-form-areaumgeschaltetis-disabledwird ausjs-inputentfernt, falls vorhanden.
Nehmen wir die notwendigen JavaScript-Änderungen vor
// Grab all elements with data-active attribute
var elems = document.querySelectorAll("[data-class][data-class-element]");
// closestParent helper function
closestParent = function(child, match) {
if (!child || child == document) {
return null;
}
if (child.classList.contains(match) || child.nodeName.toLowerCase() == match) {
return child;
}
else {
return closestParent(child.parentNode, match);
}
}
// Loop through if any are found
for(var i = 0; i < elems.length; i++){
// Add event listeners to each one
elems[i].addEventListener("click", function(e){
// Prevent default action of element
e.preventDefault();
// Grab classes list and convert to array
var dataClass = this.getAttribute('data-class');
dataClass = dataClass.split(", ");
// Grab linked elements list and convert to array
var dataClassElement = this.getAttribute('data-class-element');
dataClassElement = dataClassElement.split(", ");
// Grab data-class-behaviour list if present and convert to array
if(this.getAttribute("data-class-behaviour")) {
var dataClassBehaviour = this.getAttribute("data-class-behaviour");
dataClassBehaviour = dataClassBehaviour.split(", ");
}
// Grab data-scope list if present and convert to array
if(this.getAttribute("data-class-scope")) {
var dataClassScope = this.getAttribute("data-class-scope");
dataClassScope = dataClassScope.split(", ");
}
// Loop through all our dataClassElement items
for(var b = 0; b < dataClassElement.length; b++) {
// Grab elem references, apply scope if found
if(dataClassScope && dataClassScope[b] !== "false") {
// Grab parent
var elemParent = closestParent(this, dataClassScope[b]),
// Grab all matching child elements of parent
elemRef = elemParent.querySelectorAll("." + dataClassElement[b]);
// Convert to array
elemRef = Array.prototype.slice.call(elemRef);
// Add parent if it matches the data-class-element and fits within scope
if(dataClassScope[b] === dataClassElement[b] && elemParent.classList.contains(dataClassElement[b])) {
elemRef.unshift(elemParent);
}
}
else {
var elemRef = document.querySelectorAll("." + dataClassElement[b]);
}
// Grab class we will add
var elemClass = dataClass[b];
// Grab behaviour if any exists
if(dataClassBehaviour) {
var elemBehaviour = dataClassBehaviour[b];
}
// Do
for(var c = 0; c < elemRef.length; c++) {
if(elemBehaviour === "add") {
if(!elemRef[c].classList.contains(elemClass)) {
elemRef[c].classList.add(elemClass);
}
}
else if(elemBehaviour === "remove") {
if(elemRef[c].classList.contains(elemClass)) {
elemRef[c].classList.remove(elemClass);
}
}
else {
elemRef[c].classList.toggle(elemClass);
}
}
}
});
}
Und schließlich ein funktionierendes Beispiel
Siehe den Stift #6) Formular-Komponente mit verbesserter wiederverwendbarer Funktion für mehrere Klassen und Verhaltensweisen von Luke Harrison (@lukedidit) auf CodePen.
Abschluss
Was wir geschaffen haben, ist eine leistungsstarke Funktion, die immer wieder ohne zusätzlichen Code wiederverwendet werden kann. Sie ermöglicht es uns, schnell Logiken zum Hinzufügen, Entfernen oder Umschalten mehrerer zustandsbezogener Klassen per Klick zuzuweisen und diese Änderungen auf den gewünschten Bereich zu beschränken.
Es gibt noch viele Möglichkeiten, diese wiederverwendbare Funktion weiter zu verbessern
- Unterstützung für die Verwendung anderer Ereignisse als Klick.
- Wischunterstützung für Touch-Geräte.
- Eine Art einfacher Validierung, die es Ihnen erlaubt, JavaScript-Variablen zu deklarieren, die wahr sein müssen, bevor eine Klassenänderung vorgenommen wird.
In der Zwischenzeit, wenn Sie Ideen für eigene Verbesserungen oder sogar eine völlig andere Methode zur Verwaltung von zustandsbezogenen Klassen haben, lassen Sie es mich bitte in den Kommentaren unten wissen.
Artikelserie
- Verwaltung von Zuständen in CSS mit wiederverwendbaren JavaScript-Funktionen (Sie sind hier!)
- Aufgreifen der Ideen in diesem Artikel
Ich sehe, Sie verwenden die classList.toggle-Funktion, was bedeutet, dass Sie die neueste Version des Standards verwenden. In diesem Fall können Sie die classList.contains-Funktionen entfernen, da classList.add die Klasse nur hinzufügt, wenn sie noch nicht vorhanden ist, und classList.remove sie nur entfernt, wenn sie bereits vorhanden ist.
Sie könnten auch eine winzige switch-Anweisung anstelle der if...else if...else-Anweisung verwenden, aber das ist persönliche Präferenz ;)
Lassen Sie mich noch eine kleine Kleinigkeit einfügen: Die if (elems.length) um die for (...) sollte nicht notwendig sein, da 0 < 0 und 0 < undefined beide falsch sind, wenn ich mich nicht irre.
Danke, ich bin sicher, es gibt viele Wege, wie bessere Entwickler dies effizienter gestalten können, solange die Aussage des Artikels rüberkommt, haha.
Eine weitere Korrektur, auf die mich jemand auf Twitter aufmerksam gemacht hat, war die Entfernung der unnötigen Längenprüfungen hier und da! Diese habe ich aber jetzt entfernt.
Das ist ziemlich nett! Was denken Sie über die feine Linie zwischen so etwas und einem vollständigen Frontend-Anwendungsframework, insbesondere im Hinblick auf die zusätzlichen Funktionen, die Sie angesprochen haben, wie die Validierung von JS-Variablen?
Ich denke, das käme auf den Kontext an.
Bei einem neuen Projekt, mit einem Entwickler, der sich mit einem Frontend-Framework wie React auskennt, wäre das immer die beste Option.
Für Projekte, bei denen die Verwendung eines Frameworks wie React keine Option ist – zum Beispiel, wenn ein Entwickler an einer bestehenden Codebasis arbeitet oder wenn der Entwickler einfach nicht über ausreichend Wissen verfügt, um etwas wie React richtig zu nutzen –, kommt eine einfachere Lösung wie die oben genannte zum Einsatz.
Ich denke, das ist interessant, wenn man ein Framework bauen würde. Aber für den Anwendungsfall eines kleineren Projekts, bei dem man kein Framework verwendet, würde dies wahrscheinlich mehr Boilerplate hinzufügen, als man spart. Es ist immer interessant, neue Denkweisen zu sehen. Ein kleines Problem, das ich bei der Hilfsfunktion closestParent gefunden habe, ist, dass sie die Suche nach einem Elternteil bei sich selbst beginnt. Vielleicht haben Sie einen Grund dafür, aber ich glaube nicht, dass die jQuery-Version so funktioniert (korrigieren Sie mich, wenn ich falsch liege, ich habe jQuery schon länger nicht mehr verwendet) und es wäre ein Problem, wenn Sie zum Beispiel die Eltern-DIV wollen und das Kind auch in einer DIV ist (würde sich selbst zurückgeben). Eine einfache Lösung wäre
Obwohl der Klassenname
is-activemir wegen seiner Redundanz ein wenig die Zähne zieht ;-), ist mein Hauptkritikpunkt, dass Sie hier *nur* Klassennamen umschalten.Aus Barrierefreiheitsgründen sollten Beispiele wie Ihr Akkordeon die entsprechenden ARIA-Attribute verwenden. Und sobald Sie das
aria-expanded-Attribut umschalten, um anzuzeigen, welches Akkordeon-Element aktiv ist, wird der Klassennameis-activenicht nur im Namen, sondern vollständig redundant – da die Gestaltung der Elemente dann einfach den Attributselektor verwenden kann, um im erweiterten Zustand unterschiedliche Formatierungen anzuwenden.Ich gebe zu, dass ich über ARIA-Attribute nicht besonders gut informiert bin, also könnten Sie etwas auf dem richtigen Weg sein. Ich werde mich auf jeden Fall darum kümmern. :)
Ich stimme zu, ich habe selbst mit dem SMACSS-Ansatz begonnen, bin aber, wenn möglich, zu ARIA gewechselt. Es gibt jedoch einige kleinere Schönheitsfehler. Mehr zu meinem Ansatz für Zustände finden Sie hier: http://ecss.io/chapter6.html
Ich denke, das hätte einige Probleme für Elemente, die zu einem späteren Zeitpunkt zum DOM hinzugefügt werden. In diesem Fall müssten Sie das Zuweisen des Klickereignisses wiederholen... Habe ich Recht?
Wenn DOM-Elemente später hinzugefügt werden, muss es eine JS-Methode geben, die dies tut. Was bedeutet, dass Sie einen Auslöser für die Registrierung neuer Elemente haben.
Wie wäre es mit data-click-registered=”1″?
Sollte nicht zu schwer sein, zu filtern…
Danke Phil. Ja, das ist in der Tat ein schöner Ansatz. :) Ich habe nur gefragt, um sicherzugehen, dass ich es richtig verstanden habe.
Ein oft vernachlässigter Aspekt ist die **Trennung von Belangen** und **Fehlersuche**: Erwägen Sie die Verwendung der Live-DOM-Debugging-Ansicht des Browsers. Die Verwendung von Datenattributen für Zustände macht es viel einfacher zu verstehen.
Ist es ein Datenattribut? Dann muss es ein von JavaScript gesteuerter Zustand sein.
Ist es ein Klassenname oder ein Inline-Stil? Dann kann es nur aus HTML stammen.
Welches Modul hat den Zustand gesetzt? Finden Sie einfach den Attributnamen in Ihrem JS/Module-Verzeichnis.
Zum Testen den zustandsbestimmenden Zustand anpassen? Ändern Sie einfach den Wert des Datenattributs.
Dies führt zu strengen Regeln
JavaScript setzt niemals Klassennamen.
JavaScript setzt nur generische Stile wie „display“ oder „visibility“.
JavaScript schreibt Zustände als Datenattribute.
Zustandsnamen sind so eindeutig wie möglich. Noch besser: Implementieren Sie ein Benennungsschema als Bindeglied zwischen JS-Modulen und ihren Datenattributen.
Setzen Sie zustandsbestimmende Datenattribute so nah wie möglich an ihre wirkenden Elemente. Das bedeutet: Schreiben Sie nicht alle Anwendungszustände in das Body-Element. Setzen Sie sie stattdessen containerweise. Einige Browsersituationen (z. B. IE mit komplexen DOMs) werden von der Leistung profitieren.
CSS visualisiert Zustände.
Unterschiedliche Belange verwenden unterschiedliche Quellcodeordner.
Natürlich setzen einige vorgefertigte Frameworks/Bibliotheken (z. B. iScroll) Stile aus JS, aber das kann leicht vom eigenen Code getrennt werden.
Hallo Luke,
Danke für den Artikel.
Betrachtet man den aktuellen Stand des Webs, sind all diese Techniken wie BEM, SMACSS, OOCSS irgendwie veraltet, wenn man anfängt, JavaScript zu schreiben. Es gibt eine Menge CSS-in-JS-Techniken, wie https://github.com/css-modules/css-modules oder https://github.com/cssinjs/jss und viele mehr, die darauf abzielen, bei CSS-Schwierigkeiten zu helfen.
Auch der Code, den Sie gezeigt haben, ist gut für eine Präsentation der Idee, aber sehr schwer in der Produktion zu warten (sauber und übersichtlich zu halten). An diesem Punkt braucht jeder, der dies nutzen möchte, eine Art Bibliothek, um die Ansicht (HTML, das zur Anzeige eines Zustands verwendet wird) zu steuern. Ich würde das bekannte React vorschlagen oder Sie könnten den neuen Hype namens Vue nehmen. Damit bin ich sicher, dass Sie diesen wichtigen Punkt erwähnt haben.
Ich habe eine ähnliche Technik seit einiger Zeit verwendet (wenn auch mit anderen Attributnamen). Im Laufe der Jahre bin ich auf einige Kopfschmerzen gestoßen
Wenn Ihre Inhalte dynamisch geladen werden (z. B. ReactJS), müssen Sie ständig den DOM abfragen und die Event-Listener neu anwenden.
Das HTML beginnt sehr überladen/unordentlich auszusehen. Besonders nachdem Sie angefangen haben, sich andere scheinbar großartige data-* HTML-APIs auszudenken und hinzuzufügen.
Wenn Sie sich entscheiden, dass Ihre Implementierung angepasst werden muss, müssen Sie jede einzelne HTML-Datei ändern, in der dies vorkommt. Das kann zu einem Albtraum werden, wenn es über Hunderte von HTML-Dateien an mehreren Orten verteilt ist.
Ich bin inzwischen wieder dazu übergegangen, diese Logik in eine einzige wiederverwendbare JavaScript-Funktion zu abstrahieren. Mein Zwangsstörung mag das aufgeräumte HTML.