Wie man alle benutzerdefinierten Eigenschaften auf einer Seite in JavaScript abruft

Avatar of Tyler Gaw
Tyler Gaw am

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

Wir können JavaScript verwenden, um den Wert einer benutzerdefinierten CSS-Eigenschaft abzurufen. Robin hat eine detaillierte Erklärung dazu in Get a CSS Custom Property Value with JavaScript geschrieben. Zur Wiederholung, sagen wir, wir haben eine einzelne benutzerdefinierte Eigenschaft auf dem HTML-Element deklariert

html {
  --color-accent: #00eb9b;
}

In JavaScript können wir mit getComputedStyle und getPropertyValue auf den Wert zugreifen

const colorAccent = getComputedStyle(document.documentElement)
  .getPropertyValue('--color-accent'); // #00eb9b

Perfekt. Jetzt haben wir Zugriff auf unsere Akzentfarbe in JavaScript. Wissen Sie, was cool ist? Wenn wir diese Farbe in CSS ändern, wird sie auch in JavaScript aktualisiert! Praktisch.

Was passiert aber, wenn wir nicht nur auf eine, sondern auf viele Eigenschaften in JavaScript zugreifen müssen?

html {
  --color-accent: #00eb9b;
  --color-accent-secondary: #9db4ff;
  --color-accent-tertiary: #f2c0ea;
  --color-text: #292929;
  --color-divider: #d7d7d7;
}

Am Ende haben wir JavaScript, das so aussieht

const colorAccent = getComputedStyle(document.documentElement).getPropertyValue('--color-accent'); // #00eb9b
const colorAccentSecondary = getComputedStyle(document.documentElement).getPropertyValue('--color-accent-secondary'); // #9db4ff
const colorAccentTertiary = getComputedStyle(document.documentElement).getPropertyValue('--color-accent-tertiary'); // #f2c0ea
const colorText = getComputedStyle(document.documentElement).getPropertyValue('--color-text'); // #292929
const colorDivider = getComputedStyle(document.documentElement).getPropertyValue('--color-text'); // #d7d7d7

Wir wiederholen uns viel. Wir könnten jede dieser Zeilen verkürzen, indem wir die gemeinsamen Aufgaben in eine Funktion auslagern.

const getCSSProp = (element, propName) => getComputedStyle(element).getPropertyValue(propName);
const colorAccent = getCSSProp(document.documentElement, '--color-accent'); // #00eb9b
// repeat for each custom property...

Das hilft zwar, Wiederholungen im Code zu reduzieren, aber die Situation ist immer noch nicht ideal. Jedes Mal, wenn wir eine benutzerdefinierte Eigenschaft in CSS hinzufügen, müssen wir eine weitere Zeile JavaScript schreiben, um darauf zuzugreifen. Dies kann funktionieren und tut es auch, wenn wir nur wenige benutzerdefinierte Eigenschaften haben. Ich habe dieses Setup bereits in Produktionsprojekten verwendet. Aber es ist auch möglich, dies zu automatisieren.

Gehen wir den Prozess der Automatisierung durch, indem wir ein funktionierendes Ding erstellen.

Was erstellen wir?

Wir erstellen eine Farbpalette, die ein übliches Merkmal in Musterbibliotheken ist. Wir generieren ein Raster von Farbfeldern aus unseren benutzerdefinierten CSS-Eigenschaften. 

Hier ist die vollständige Demo, die wir Schritt für Schritt erstellen werden.

A preview of our CSS custom property-driven color palette. Showing six cards, one for each color, including the custom property name and hex value in each card.
Hier ist, was wir anstreben.

Legen wir den Grundstein. Wir verwenden eine ungeordnete Liste, um unsere Palette anzuzeigen. Jedes Farbfeld ist ein <li>-Element, das wir mit JavaScript rendern werden. 

<ul class="colors"></ul>

Die CSS für das Grid-Layout ist für die Technik in diesem Beitrag nicht relevant, daher werden wir sie nicht im Detail betrachten. Sie ist in der CodePen-Demo verfügbar.

Nachdem wir nun unser HTML und CSS haben, konzentrieren wir uns auf das JavaScript. Hier ist eine Übersicht, was wir mit unserem Code machen werden

  1. Alle Stylesheets auf einer Seite abrufen, sowohl externe als auch interne
  2. Alle Stylesheets von Drittanbieter-Domains verwerfen
  3. Alle Regeln für die verbleibenden Stylesheets abrufen
  4. Alle Regeln verwerfen, die keine einfachen Stilregeln sind
  5. Den Namen und Wert aller CSS-Eigenschaften abrufen
  6. Nicht-benutzerdefinierte CSS-Eigenschaften verwerfen
  7. HTML erstellen, um die Farbfelder anzuzeigen

Lasst uns loslegen.

Schritt 1: Alle Stylesheets auf einer Seite abrufen

Das Erste, was wir tun müssen, ist, alle externen und internen Stylesheets auf der aktuellen Seite abzurufen. Stylesheets sind als Member des globalen `document` verfügbar.

document.styleSheets

Das gibt ein Array-ähnliches Objekt zurück. Wir wollen Array-Methoden verwenden, also konvertieren wir es in ein Array. Packen wir das auch in eine Funktion, die wir in diesem Beitrag verwenden werden.

const getCSSCustomPropIndex = () => [...document.styleSheets];

Wenn wir `getCSSCustomPropIndex` aufrufen, sehen wir ein Array von CSSStyleSheet-Objekten, eines für jedes externe und interne Stylesheet auf der aktuellen Seite.

The output of getCSSCustomPropIndex, an array of CSSStyleSheet objects

Schritt 2: Drittanbieter-Stylesheets verwerfen

Wenn unser Skript auf https://example.com läuft, muss jedes Stylesheet, das wir inspizieren wollen, auch auf https://example.com liegen. Dies ist eine Sicherheitsfunktion. Aus der MDN-Dokumentation für CSSStyleSheet

In einigen Browsern führt der Zugriff auf cssRules zu einem SecurityError, wenn ein Stylesheet von einer anderen Domain geladen wird.

Das bedeutet, dass wir, wenn die aktuelle Seite auf ein Stylesheet verweist, das auf https://some-cdn.com gehostet wird, keine benutzerdefinierten Eigenschaften – oder überhaupt keine Stile – daraus abrufen können. Der Ansatz, den wir hier verfolgen, funktioniert nur für Stylesheets, die auf der aktuellen Domain gehostet werden.

CSSStyleSheet-Objekte haben eine href-Eigenschaft. Ihr Wert ist die vollständige URL zum Stylesheet, z. B. https://example.com/styles.css. Interne Stylesheets haben eine href-Eigenschaft, aber der Wert ist null.

Schreiben wir eine Funktion, die Drittanbieter-Stylesheets verwirft. Das tun wir, indem wir den href-Wert des Stylesheets mit dem current location.origin vergleichen.

const isSameDomain = (styleSheet) => {
  if (!styleSheet.href) {
    return true;
  }


  return styleSheet.href.indexOf(window.location.origin) === 0;
};

Jetzt verwenden wir `isSameDomain` als Filter für `document.styleSheets`.

const getCSSCustomPropIndex = () => [...document.styleSheets]
  .filter(isSameDomain);

Nachdem die Drittanbieter-Stylesheets verworfen wurden, können wir den Inhalt der verbleibenden inspizieren.

Schritt 3: Alle Regeln für die verbleibenden Stylesheets abrufen

Unser Ziel für `getCSSCustomPropIndex` ist es, ein Array von Arrays zu erzeugen. Um dorthin zu gelangen, werden wir eine Kombination von Array-Methoden verwenden, um zu durchlaufen, die gewünschten Werte zu finden und sie zu kombinieren. Machen wir einen ersten Schritt in diese Richtung, indem wir ein Array erzeugen, das jede Stilregel enthält.

const getCSSCustomPropIndex = () => [...document.styleSheets]
  .filter(isSameDomain)
  .reduce((finalArr, sheet) => finalArr.concat(...sheet.cssRules), []);

Wir verwenden `reduce` und `concat`, weil wir ein flaches Array erzeugen wollen, bei dem jedes Element erster Ebene das ist, was uns interessiert. In diesem Ausschnitt durchlaufen wir einzelne CSSStyleSheet-Objekte. Für jedes von ihnen benötigen wir dessen cssRules. Aus der MDN-Dokumentation

Die schreibgeschützte Eigenschaft `cssRules` von `CSSStyleSheet` gibt eine lebende `CSSRuleList` zurück, die eine Echtzeit-Aktualisierung aller CSS-Regeln liefert, aus denen das Stylesheet besteht. Jedes Element in der Liste ist eine `CSSRule`, die eine einzelne Regel definiert.

Jede CSS-Regel ist der Selektor, die geschweiften Klammern und die Eigenschaftsdeklarationen. Wir verwenden den Spread-Operator `...sheet.cssRules`, um jede Regel aus dem `cssRules`-Objekt zu nehmen und in `finalArr` zu platzieren. Wenn wir die Ausgabe von `getCSSCustomPropIndex` protokollieren, erhalten wir ein einstufiges Array von CSSRule-Objekten.

Example output of getCSSCustomPropIndex producing an array of CSSRule objects

Damit erhalten wir alle CSS-Regeln für alle Stylesheets. Wir wollen einige davon verwerfen, also machen wir weiter.

Schritt 4: Alle Regeln verwerfen, die keine einfachen Stilregeln sind

CSS-Regeln gibt es in verschiedenen Typen. CSS-Spezifikationen definieren jeden Typ mit einem konstanten Namen und einer Ganzzahl. Der häufigste Regeltyp ist `CSSStyleRule`. Ein weiterer Regeltyp ist `CSSMediaRule`. Wir verwenden diese, um Media Queries zu definieren, wie z.B. `@media (min-width: 400px) {}`. Andere Typen umfassen `CSSSupportsRule`, `CSSFontFaceRule` und `CSSKeyframesRule`. Sehen Sie sich die Typ-Konstanten-Sektion der MDN-Dokumentation für CSSRule an, um die vollständige Liste zu erhalten.

Wir interessieren uns nur für Regeln, bei denen wir benutzerdefinierte Eigenschaften definieren, und für die Zwecke dieses Beitrags konzentrieren wir uns auf CSSStyleRule. Das schließt den Regeltyp CSSMediaRule aus, bei dem es gültig ist, benutzerdefinierte Eigenschaften zu definieren. Wir könnten einen ähnlichen Ansatz wie den, den wir hier verwenden, um benutzerdefinierte Eigenschaften zu extrahieren, aber wir werden diesen spezifischen Regeltyp ausschließen, um den Umfang der Demo zu begrenzen.

Um unseren Fokus auf Stilregeln zu beschränken, schreiben wir einen weiteren Array-Filter

const isStyleRule = (rule) => rule.type === 1;

Jede `CSSRule` hat eine `type`-Eigenschaft, die die Ganzzahl für diesen Typkonstanten zurückgibt. Wir verwenden `isStyleRule`, um `sheet.cssRules` zu filtern.

const getCSSCustomPropIndex = () => [...document.styleSheets]
  .filter(isSameDomain)
  .reduce((finalArr, sheet) => finalArr.concat(
    [...sheet.cssRules].filter(isStyleRule)
  ), []);

Eine Sache, die zu beachten ist, ist, dass wir `...sheet.cssRules` in Klammern einschließen, damit wir die Array-Methode `filter` verwenden können.

Unser Stylesheet enthielt nur `CSSStyleRules`, daher sind die Demoergebnisse die gleichen wie zuvor. Wenn unser Stylesheet Media Queries oder `font-face`-Deklarationen enthielte, würde `isStyleRule` diese verwerfen.

Schritt 5: Name und Wert aller Eigenschaften abrufen

Nachdem wir nun die gewünschten Regeln haben, können wir die Eigenschaften abrufen, aus denen sie bestehen. `CSSStyleRule`-Objekte haben eine `style`-Eigenschaft, die ein CSSStyleDeclaration-Objekt ist. Es besteht aus Standard-CSS-Eigenschaften wie `color`, `font-family` und `border-radius` sowie benutzerdefinierten Eigenschaften. Fügen wir das unserer `getCSSCustomPropIndex`-Funktion hinzu, damit sie jede Regel betrachtet und dabei ein Array von Arrays aufbaut

const getCSSCustomPropIndex = () => [...document.styleSheets]
  .filter(isSameDomain)
  .reduce((finalArr, sheet) => finalArr.concat(
    [...sheet.cssRules]
      .filter(isStyleRule)
      .reduce((propValArr, rule) => {
        const props = []; /* TODO: more work needed here */
        return [...propValArr, ...props];
      }, [])
  ), []);

Wenn wir dies jetzt aufrufen, erhalten wir ein leeres Array. Wir haben noch mehr Arbeit vor uns, aber das legt das Fundament. Da wir mit einem Array enden wollen, beginnen wir mit einem leeren Array, indem wir den Akkumulator verwenden, der der zweite Parameter von `reduce` ist. Im Körper der `reduce`-Callback-Funktion haben wir eine Platzhaltervariable, `props`, wo wir die Eigenschaften sammeln werden. Die `return`-Anweisung kombiniert das Array aus der vorherigen Iteration – den Akkumulator – mit dem aktuellen `props`-Array.

Im Moment sind beide leere Arrays. Wir müssen `rule.style` verwenden, um `props` mit einem Array für jede Eigenschaft/Wert in der aktuellen Regel zu füllen

const getCSSCustomPropIndex = () => [...document.styleSheets]
  .filter(isSameDomain)
  .reduce((finalArr, sheet) => finalArr.concat(
    [...sheet.cssRules]
      .filter(isStyleRule)
      .reduce((propValArr, rule) => {
        const props = [...rule.style].map((propName) => [
          propName.trim(),
          rule.style.getPropertyValue(propName).trim()
        ]);
        return [...propValArr, ...props];
      }, [])
  ), []);

`rule.style` ist Array-ähnlich, also verwenden wir erneut den Spread-Operator, um jedes Element daraus in ein Array zu legen, das wir mit `map` durchlaufen. Im `map`-Callback geben wir ein Array mit zwei Elementen zurück. Das erste Element ist `propName` (das `color`, `font-family`, `--color-accent` usw. enthält). Das zweite Element ist der Wert jeder Eigenschaft. Um diesen zu erhalten, verwenden wir die getPropertyValue-Methode von CSSStyleDeclaration. Sie nimmt einen einzelnen Parameter, den String-Namen der CSS-Eigenschaft. 

Wir verwenden `trim` für sowohl den Namen als auch den Wert, um sicherzustellen, dass wir keine führenden oder nachfolgenden Leerzeichen einschließen, die manchmal zurückbleiben.

Jetzt, da wir `getCSSCustomPropIndex` aufrufen, erhalten wir ein Array von Arrays. Jedes Kind-Array enthält einen CSS-Eigenschaftsnamen und einen Wert.

Output of getCSSCustomPropIndex showing an array of arrays containing every property name and value

Das ist es, wonach wir suchen! Nun, fast. Wir erhalten jede Eigenschaft zusätzlich zu den benutzerdefinierten Eigenschaften. Wir brauchen noch einen Filter, um diese Standardeigenschaften zu entfernen, denn wir wollen nur die benutzerdefinierten Eigenschaften.

Schritt 6: Nicht-benutzerdefinierte Eigenschaften verwerfen

Um festzustellen, ob eine Eigenschaft benutzerdefiniert ist, können wir uns den Namen ansehen. Wir wissen, dass benutzerdefinierte Eigenschaften mit zwei Bindestrichen (`--`) beginnen müssen. Das ist im CSS-Bereich einzigartig, daher können wir es verwenden, um eine Filterfunktion zu schreiben

([propName]) => propName.indexOf("--") === 0)

Dann verwenden wir sie als Filter für das `props`-Array

const getCSSCustomPropIndex = () =>
  [...document.styleSheets].filter(isSameDomain).reduce(
    (finalArr, sheet) =>
      finalArr.concat(
        [...sheet.cssRules].filter(isStyleRule).reduce((propValArr, rule) => {
          const props = [...rule.style]
            .map((propName) => [
              propName.trim(),
              rule.style.getPropertyValue(propName).trim()
            ])
            .filter(([propName]) => propName.indexOf("--") === 0);


          return [...propValArr, ...props];
        }, [])
      ),
    []
  );

In der Funktionssignatur haben wir `([propName])`. Dort verwenden wir Array-Destrukturierung, um auf das erste Element jedes Kind-Arrays in `props` zuzugreifen. Von dort aus führen wir eine `indexOf`-Prüfung für den Namen der Eigenschaft durch. Wenn `--` nicht am Anfang des Eigenschaftsnamens steht, dann nehmen wir ihn nicht in das `props`-Array auf.

Wenn wir das Ergebnis protokollieren, erhalten wir die exakte Ausgabe, die wir suchen: Ein Array von Arrays für jede benutzerdefinierte Eigenschaft und ihren Wert ohne andere Eigenschaften.

The output of getCSSCustomPropIndex showing an array of arrays containing every custom property and its value

Wenn wir weiter in die Zukunft blicken, muss das Erstellen der Eigenschaft/Wert-Zuordnung nicht so viel Code erfordern. Es gibt eine Alternative im Entwurf der CSS Typed Object Model Level 1, der CSSStyleRule.styleMap verwendet. Die Eigenschaft `styleMap` ist ein Array-ähnliches Objekt jeder Eigenschaft/jedes Werts einer CSS-Regel. Wir haben sie noch nicht, aber wenn wir sie hätten, könnten wir unseren obigen Code verkürzen, indem wir `map` entfernen

// ...
const props = [...rule.styleMap.entries()].filter(/*same filter*/);
// ...

Zum Zeitpunkt des Schreibens haben Chrome und Edge Implementierungen von `styleMap`, aber keine anderen großen Browser. Da `styleMap` in einem Entwurf vorliegt, gibt es keine Garantie, dass wir sie tatsächlich erhalten werden, und es hat keinen Sinn, sie für diese Demo zu verwenden. Dennoch ist es schön zu wissen, dass es eine zukünftige Möglichkeit ist!

Wir haben die gewünschte Datenstruktur. Jetzt verwenden wir die Daten, um Farbfelder anzuzeigen.

Schritt 7: HTML zum Anzeigen der Farbfelder erstellen

Die Daten in die exakt benötigte Form zu bringen, war die harte Arbeit. Wir brauchen noch ein weiteres Stück JavaScript, um unsere schönen Farbfelder zu rendern. Anstatt die Ausgabe von `getCSSCustomPropIndex` zu protokollieren, speichern wir sie in einer Variablen.

const cssCustomPropIndex = getCSSCustomPropIndex();

Hier ist das HTML, das wir verwendet haben, um unser Farbfeld zu Beginn dieses Beitrags zu erstellen

<ul class="colors"></ul>

Wir werden `innerHTML` verwenden, um diese Liste mit einem Listenelement für jede Farbe zu füllen

document.querySelector(".colors").innerHTML = cssCustomPropIndex.reduce(
  (str, [prop, val]) => `${str}<li class="color">
    <b class="color__swatch" style="--color: ${val}"></b>
    <div class="color__details">
      <input value="${prop}" readonly />
      <input value="${val}" readonly />
    </div>
   </li>`,
  "");

Wir verwenden `reduce`, um über den benutzerdefinierten Eigenschaftsindex zu iterieren und einen einzigen HTML-ähnlichen String für `innerHTML` zu erstellen. Aber `reduce` ist nicht die einzige Möglichkeit, dies zu tun. Wir könnten `map` und `join` oder `forEach` verwenden. Jede Methode zum Erstellen des Strings funktioniert hier. Dies ist einfach meine bevorzugte Methode.

Ich möchte ein paar spezifische Codeabschnitte hervorheben. In der `reduce`-Callback-Signatur verwenden wir erneut Array-Destrukturierung mit `[prop, val]`, diesmal um auf beide Elemente jedes Kind-Arrays zuzugreifen. Wir verwenden dann die Variablen `prop` und `val` im Körper der Funktion.

Um das Beispiel jeder Farbe anzuzeigen, verwenden wir ein `b`-Element mit einem Inline-Stil

<b class="color__swatch" style="--color: ${val}"></b>

Das bedeutet, dass wir am Ende HTML erhalten, das so aussieht

<b class="color__swatch" style="--color: #00eb9b"></b>

Aber wie setzt das eine Hintergrundfarbe? In dem vollständigen CSS verwenden wir die benutzerdefinierte Eigenschaft `--color` als Wert von `background-color` für jedes `.color__swatch`. Da externe CSS-Regeln von Inline-Stilen erben, ist `--color` der Wert, den wir auf dem `b`-Element setzen.

.color__swatch {
  background-color: var(--color);
  /* other properties */
}

Wir haben jetzt eine HTML-Anzeige von Farbfeldern, die unsere benutzerdefinierten CSS-Eigenschaften darstellen!


Diese Demo konzentriert sich auf Farben, aber die Technik beschränkt sich nicht auf benutzerdefinierte Farb-Props. Es gibt keinen Grund, warum wir diesen Ansatz nicht erweitern könnten, um andere Abschnitte einer Musterbibliothek zu generieren, wie z.B. Schriften, Abstände, Gittereinstellungen usw. Alles, was als benutzerdefinierte Eigenschaft gespeichert werden kann, kann mit dieser Technik automatisch auf einer Seite angezeigt werden.