inStyle (Ändern des aktuellen Selektors `&` in Sass)

Avatar of Filip Naumovic
Filip Naumovic am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Der folgende Beitrag stammt von Filip Naumovic von Salsita Software. Filip hat ein Sass-Tool entwickelt, das bei einem Problem hilft, das ich selbst schon oft erlebt habe. Du verschachtelst fröhlich in Sass. Du bist vielleicht ein oder zwei Ebenen tief und musst eine Variation gestalten, die auf einem übergeordneten Selektor basiert. Du musst entweder aus der Verschachtelung ausbrechen und einen neuen Verschachtelungskontext starten oder zu `@at-root` greifen. Ich überlasse es Filip, die Geschichte seines neuen Tools zu erzählen, das das ändert.

Sass hat vielen von uns enorm geholfen, unsere CSS-Codebasen aufzuräumen. Als es auf den Markt kam, entstanden einige äußerst nützliche Muster, die schnell übernommen wurden.

Zum Beispiel ermöglichten die Verwendung von & und die Verschachtelung die Bereinigung von sonst ziemlich unschönen Codeblöcken.

Das hier

.my-app { display: block; }
.my-app .widget { border-radius: 5px; }
.my-app .widget.blue { color: blue; }
.isIE6 .my-app .widget { background-image: url('fake-borders.png'); }
@media (max-width: 768px) { .my-app .widget { float: left; } }

Verwandelt in dies

.my-app {
  display: block;
  .widget {
    border-radius: 5px;
    &.blue {
      color: blue;
    }
    .isIE6 & {
      background-image: url("fake-borders.png");
    }
    @media (max-width: 768px) {
      float: left;
    }
  }
}

Was für eine positive Veränderung für unsere geistige Gesundheit!

Alle Stilvariationen des .widget-Elements sind klar unter ihm verschachtelt, wobei die Einrückung sowohl als visueller Hinweis auf die Relevanz als auch als Abfragegenerator dient.

Der aktuelle Selektor (&) bietet in diesem Fall eine Abkürzung für die gängigsten Muster. Das Gestalten der Variation eines Elements, das entweder durch eine Eigenschaft des Elements selbst oder einen vorangestellten übergeordneten Zustand aufgerufen wird.

Verschachtelte Media Queries sind eine neuere Ergänzung, deuten aber darauf hin, dass die Entwicklung hin zu einer eingerückten Syntax für Stile fast zwangsläufig erfolgt. Sie ist leicht zu lesen und zu navigieren, da sie irgendwie die vertraute DOM-Struktur widerspiegelt und alle Stile für ein Element an einem Ort hält, während sie dennoch unsere wertvollen, aber manchmal komplizierten Selektoren erzeugt.

Heute sind Verschachtelungs- und aktuelle Selektorfunktionen in Less, Sass und Stylus vorhanden. Mit etwas Wein könnte man es fast als Standard bezeichnen.

Ein klassischer Fall von „Das kannst du nicht tun.“

Am obigen Codeblock als Beispiel, fügen wir Stile für .my-app.expanded .widget hinzu.

Trotz unserer mächtigen Werkzeuge finden wir uns schnell mit begrenzten Wahlmöglichkeiten wieder

Option 1

Mit der modernen @at-root-Direktive (oder / in Stylus) verlassen wir den aktuellen Geltungsbereich vollständig und wiederholen die vollständige Wurzelabfrage, um die relevanten neuen Stile unter .widget verschachtelt zu halten, da der aktuelle Selektor uns nicht helfen kann, diese Beziehung auszudrücken.

.my-app {
  display: block;
  .widget {
    border-radius: 5px;
    &.blue {
      color: blue;
    }
    .isIE6 & {
      background-image: url("fake-borders.png");
    }
    // repeating the root selector here
    @at-root .my-app.expanded .widget {
      color: red'
    }
    @media (max-width: 768px) {
      float: left;
    }
  }
}

Dies erzeugt schwerer lesbaren Code mit viel Duplizität, insbesondere wenn die reale Nutzung weit über unser kleines Beispielstück hinausgeht. Aber es hält unser glorreiches Verschachtelungs-Paradigma intakt.

Option 2

Wir erstellen einen neuen Codeblock unter .my-app und verwenden ihn, um alle Kindelemente zu ändern, die für den .expanded-Zustand relevant sind. Das bedeutet, dass unser .widget nun an verschiedenen Stellen gestylt wird, und diese Trennung wächst mit jedem hinzugefügten Zustand in jedem Element der Verschachtelung.

.my-app {
  display: block;
  .widget {
    border-radius: 5px;
    &.blue {
      color: blue;
    }
    .isIE6 & {
      background-image: url("fake-borders.png");
    }
    @media (max-width: 768px) {
      float: left;
    }
  }
  &.expanded .widget
     color: red;
  }
}

Obwohl es direkt gegen unseren Traum vom „Verschachteln aller relevanten Stile“ verstößt, ist es die Unvollkommenheit, mit der wir gelernt haben zu leben. Viele von Ihnen würden dieses Muster wahrscheinlich sogar verteidigen, da es seit einiger Zeit so gehandhabt wird.

Wäre es jedoch nicht großartig, für die Wahlfreiheit eine Option 3 zu haben? Eine, die es uns ermöglicht, die einfache Änderung in .my-app.expanded auszudrücken, die unseren .widget beeinflusst, ohne den Kontext verlassen zu müssen?

Diese Idee hat mich eine ganze Weile heimlich beschäftigt, wenn auch nur aus einer gewissen Form von OCD bezüglich meiner eigenen Stylesheets. Ich habe es zu meiner Nebenaufgabe gemacht, dieses fehlende Werkzeug im Werkzeugkasten für Stile zu finden.

Option 3 finden

Bei der Recherche zu diesem Thema habe ich Spinnweben, ewige Diskussionen und wild variierende Vorschläge gefunden, von denen viele eine Ergänzung einer speziellen Syntax für den aktuellen Selektor & vorschlugen. Dies würde Monate des Lernens komplizierter Kernbibliotheken und das Kämpfen mit dem langen Krieg bedeuten, was sich sofort wie eine unerträgliche Last anfühlte.

Zweitens denke ich, dass & gut funktioniert, da es eine klare Darstellung des gesamten Kontexts ist, und aus diesem Grund könnte es problematisch sein, ihm weitere Funktionen hinzuzufügen. Es tut eine Sache und tut sie gut, daher schien es zu diesem Zeitpunkt eine bessere Idee zu sein, einen guten Partner dafür zu schaffen.

Um die einfache Integration zu ermöglichen, habe ich beschlossen, die Idee auf der Ebene der Präprozessorsprache zu implementieren, damit du sie einfach @importieren und sofort verwenden kannst. Präprozessoren sind heutzutage leistungsstarke Frameworks, warum also nicht?

Meine erste Wahl war Stylus, weil es einfach so genial ist. Leider kann, aufgrund von Issue 1703, der Platzhalter für den aktuellen Selektor derzeit nicht innerhalb einer Mixin-Funktion modifiziert werden. Wie ein guter Fanatiker werde ich bis ans Ende der Zeit warten, bis Stylus es behebt, aber ich musste weitersuchen, um etwas zu finden, das ich *jetzt* implementieren konnte.

Man darf den aktuellen Selektor in Less nicht parsen, also fiel das aus.

SassScript hingegen erwies sich als Kraftpaket. Obwohl ihm viele nützliche Abstraktionen für die Manipulation von Strings und Arrays fehlen, ist es sehr gut möglich, solche Funktionen manuell zu erstellen. Viele davon werden bereits von Sass Prince Kitty Giraudel bereitgestellt.

Nach Monaten des kontrollierten String-Terrors…

inStyle für Sass 3.4+ ist geboren!

Kitscher Name, ich weiß. Aber er deutet auf die Funktionalität hin, denn du möchtest, dass dieses Ding im tatsächlichen Code lesbar ist. Mixin-Syntax ist für Präprozessor-Benutzer bereits vertraut, daher klang ein suggestiver Name, um Änderungen *in* den übergeordneten Elementen zu beschreiben, für mich als zusätzliche Hürde gegen Unvertrautheit richtig.

Auf jeden Fall muss all das lesbar bleiben und komplexe Fälle behandeln, sonst verliert es seinen Zweck zugunsten von @at-root-Selektoransätzen oder dem einfachen Verschachteln des Codes an anderer Stelle. Ich habe mich für zwei grundlegende Mechanismen entschieden, von denen ich glaube, dass sie selbst die abscheulichsten Bedürfnisse adressieren und gleichzeitig einen logisch einfachen Parsing-Algorithmus beibehalten.

Verwendung 1) Modifikation

Ergänzungen zu einem zusammengesetzten Element, das im aktuellen Selektor vorhanden ist, handhaben ~80% des realen Codes, genau wie unser erstes Beispiel zu erreichen versucht.

.my-app {
  display: block;
  .widget {
    border-radius: 5px;
    &.blue {
      color: blue;
    }
    .isIE6 & {
      background-image: url("fake-borders.png");
    }
    @include in(".my-app.expanded") {
      color: red; // .my-app.expanded .widget { };
    }
    @media (max-width: 768px) {
      float: left;
    }
  }
}

Versuche, das so zu lesen

Gestalten von .widget *in* dem Zustand .my-app.expanded.

Die Funktion durchsucht die Verschachtelung von unten nach oben nach dem ersten Vorkommen eines .my-app-Elements (wobei das aktuelle Element übersprungen wird) und hängt die Klasse .expanded daran an, wobei ein neuer Selektor zurückgegeben wird.

Was ist mit längeren Abfragen und Kombinationsänderungen?

table {
  table-layout: fixed;
  thead {
    font-weight: normal;
    tr {
      height: 30px;
      td {
        background-color: #fafafa;
        &.user {
          font-weight: bold'
        }
        @include in('table.one tr.two:hover') {
          background-image: url(rainbow.png) // table.one thead tr.two:hover td { };
        }
      }
    }
  }
}

Der tr-Elternteil wird gefunden und mit .two:hover modifiziert. Wenn man nach oben geht, wird auch table gefunden und mit .one modifiziert, andere Elemente werden übersprungen.

Irrelevante Multi-Selektoren werden aus dem neuen Selektor entfernt

ul, ol {
  list-style: none;
  li {
    display: inline-block;
    a {
      text-decoration: underline;
      @include in("ol.links") {
        color: orange; // ol.links li a { };
      }
    }
  }
}

Unmögliche Fälle und ungültige CSS-Abfragen erzeugen beim Kompilieren einen blockierenden Sass-Fehler

table {
  table-layout: fixed;
  td {
    height: 30px;
    @include in("table^s&()#") {
      what: the; // ERROR, invalid CSS
    }
    @include in ("tr.green:hover") {
      border-color: green; // ERROR, no tr or tr.green to modify in &
    }
  }
}

Beim Crash-Testing im Produktionsumfeld (hah!) habe ich einen weiteren sehr praktischen Bedarf festgestellt, den ich nicht nur durch Modifikationen des übergeordneten Baums erfüllen konnte. Tatsächlich löst er das obige Beispiel, da man dies mit tr.green:hover tun können muss. Man muss einfach sagen können, wo.

Verwendung 2) Einfügung

Nehmen wir an, das Folgende

table {
  table-layout: fixed;
  thead {
    font-weight: normal;
  }
  tr {
    height: 30px;
  }
  td {
    background-color: #fafafa;
  }
}

Wo würdest du idealerweise einen table thead tr-Selektor verschachteln? Nach dem Dogma musst du ihn scheinbar wie folgt hinzufügen:

table {
  table-layout: fixed;
  thead {
    font-weight: normal;
    tr {
      height: 50px;
    }
  tr {
    height: 30px;
  }
  td {
    background-color: #fafafa;
  }
}

Das betroffene gestylte Element ist jedoch tr und du hast das bereits als generischen Stil, also könnte das Verschachteln darunter als Variante näher daran sein, wie du über die Beziehung denkst, und die Lücken füllen, die der aktuelle Selektor & nicht beschreiben kann.

In diesem Fall bedeutet dies, dass es einen einfachen Weg geben muss, einen Selektor an einer bestimmten Position über dem aktuellen Element einzufügen und gleichzeitig Kombinationen mit zusammengesetzten Modifikationen zu ermöglichen. Ich konnte mir das ohne ein Sonderzeichen nicht vorstellen, daher habe ich mich für das visuell suggestive ^ (Caret) entschieden.

table {
  table-layout: fixed;
  thead {
    font-weight: normal;
  }
  tr {
    height: 30px;
    @include in("^thead") {
      height: 50px; // table thead tr { };
    }
  }
  td {
    background-color: #fafafa;
    @include in("table.blue-skin ^tbody") {
      background-color: blue; // table.blue-skin tbody td { };
    }
  }
}

In diesem Fall fügt der Caret thead *eine* Ebene oberhalb des aktuellen oder zuletzt modifizierten Elements ein. Mehr Carets bedeuten höhere Sprünge im aktuellen Selektor

main {
  display: flex;
  > div {
    flex-grow: 1;
    span {
      display: inline-block;
      &.highlight {
        outline-style: dashed;
        @include in("^^.section.active") {
          outline-style: solid; // main .section.active > div span.highlight { };
        }
        @include in("^^^.section") {
          some: thing; // ERROR, inserting too high, it would equal to ".section &"
        }
      }
    }
  }
}

Hinweis: &.highlight ist dasselbe Element wie span, daher behandelt die Einfügung es als einen Schritt in der Verschachtelung

Ich denke, inStyle glänzt in den einfachsten Fällen, die auch bei weitem die häufigsten sind. Aber Dinge können bei Bedarf komplexer werden.

Verwendung 3) Fortgeschrittene Kombinationen

Der Matching-Algorithmus erlaubt es Ihnen, noch wilder einzufügen oder mehr zusammengesetzte Teile gleichzeitig zu modifizieren

.my-app {
  display: flex;
  section {
    flex: 1;    
    header {
      width: 100%;
      @include in("^^.wrapper ^.dialog)") {
        height: 50px; // .my-app .wrapper section .dialog header { };
      }
      @include in("^.overlay ^.sidebar") {
        position: fixed; // .my-app section .overlay .sidebar header { };
      }
      @include in("^.modifier section.next ^.parent") {
        opacity: 0; // .my-app .modifier section.next .parent header { };
      }
    }
  }
}
  1. .dialog wird eine Ebene über header eingefügt und .wrapper zwei Ebenen darüber.
  2. .sidebar wird über header eingefügt und .overlay direkt darüber.
  3. Verschiebt .parent über header, modifiziert section mit .next und schiebt dann .modifier darüber.

Das erinnert mich, vielleicht haben Sie Feedback! Ich habe darüber nachgedacht, eine einfachere Syntax zu ermöglichen, wenn Sie mehrere zusammengesetzte Elemente direkt hintereinander einfügen möchten, wie im zweiten Fall, vielleicht etwas wie @include in("^(.overlay .sidebar)") oder den Parser zu verbessern und ein natürlicheres @include in("^.overlay .sidebar") zu ermöglichen. Lassen Sie mich Ihre Meinung wissen!

Nachdem ich es eine Weile benutzt habe, habe ich festgestellt, dass die meisten meiner unbequemen Code-Muster recht einfach gelöst werden können, indem ich hier und da ein Element ändere oder einen neuen Selektor an eine bestimmte Position schiebe und die Dinge an Ort und Stelle halte. Dennoch muss ich ehrlich sein, es ist von Natur aus potenziell ziemlich invasiv für Ihre übliche Code-Organisation.

Ich kann mir vorstellen, dass die Verwendung von inStyle zu hitzigen Diskussionen führen könnte. Meine Kollegen scheinen entweder aufgeschlossen zu sein oder sich nicht zu kümmern, was beides großartig ist.

Wenn Sie es verwenden, hoffe ich, dass die richtige Handhabung wie bei jedem anderen Werkzeug erfolgt: wenn es für den Job geeignet ist. Das Spammen mit komplexen verschachtelten Mixins wird wahrscheinlich nicht mehr Lesbarkeit bieten als das Schreiben der vollständigen Abfrage von Grund auf, aber andererseits kann es die meisten realen Probleme vereinfachen und gleichzeitig einen schlanken Fußabdruck hinterlassen.

In naher Zukunft möchte ich den Stylus-Port zum Laufen bringen und vielleicht ein Atom-Editor-Plugin erstellen, um die resultierende Abfrage als Hinweis im Code anzuzeigen.

Es hat Spaß gemacht, sich an der Lösung von First-World-Problemen von CSS zu versuchen, und ich hoffe, dass Sie das Thema zumindest für diskussionswürdig halten. Das Projekt ist Open Source, also fühlen Sie sich frei, mit Code oder Feedback an Bord zu kommen!

Ob Sie es lieben oder hassen, hier ist es auf GitHub, hier ist eine kleine Microsite und hier ist ein Live-Debugger zur Sicherheit.

Siehe den Pen inStyle Crash Test Dummy von Salsita Software (@salsita) auf CodePen.

Danke fürs Lesen!