javascriptcssdynamic

Create, modify, disable multiple CSS rules by ID with JavaScript


Abstract

I'm trying to figure out a way to use JavaScript to dynamically create CSS rules on a page, modify them, and disable them. The rules would come in "packages" with an ID and a group of one or more CSS rules which may overlap.

Example and Data

/*ID: something*/
div#foobar {display:none;}

/*ID: blah*/
input {background:red;}
div.password {width:100px;}

/*ID: test*/
div.password {width:200px;}

The rules are in an associative-array that contains the data, such as:

myrules["something"] = "div#foobar {display:none;}";
myrules["blah"]      = "div.password {width:100px;} input {background:red;}";
myrules["test"]      = "div.password {width:200px;}";

"Question"

Now I need a way to add the defined rules to the page with a way to toggle them using the IDs.

Requirements

The main issues the current attempts (below) have run into are:

Attempts (what I've already tried)

I've looked at several different ways from document.styleSheets to .sheet.cssRules[], from .innerHTML to insertRule(). I've become dizzy from trying to figure out what's what; it's such a quagmire, with poor examples. Sometimes I manage to use one technique to accomplish one aspect of it, but then another aspect won't work. I can't find a solution that satisfies all of the aforementioned requirements.

And searches are difficult because of the ambiguous nature of phrasing leading to incorrect search-results.



Surely there has to be an efficient way to do this, right? 🤨


Solution

  • One further approach is below, with explanatory comments in the code:

    // using an arrow function modifyStyles() to handle the application and
    // removal of the user-selected styles, this function takes one argument
    // a reference to the Event Object (passed automagically from the
    // EventTarget.addEventListener() call, later):
    const modifyStyles = (evt) => {
      // destructuring assignment, which takes the 'target' property
      // from the Event Object, and then assigns the value of that
      // property to the 'btn' variable (this is a personal preference
      // and helps me to keep track of what interaction this function
      // handles):
      let {
        target: btn
      } = evt,
      // here we use document.querySelector() to retrieve a specific element
      // using a CSS selector, if that returns a falsey result then instead
      // we create that <style> element and assign that created element to
      // to the 'style' variable:
      style =
        document.querySelector("#user-custom-css-content") ||
        document.createElement("style");
    
      // we toggle the 'active' class on the clicked <button> element, if the
      // class is already present it will be removed, if not present it will
      // be added:
      btn.classList.toggle("active");
    
      // here we retrieve closest ancestor '.wrapper' element to the clicked
      // <button>, and from their retrieve all '.active' elements; we use the
      // spread (...) operator and Array literal to convert the returned
      // NodeList to an Array:
      let rules = [...btn.closest(".wrapper").querySelectorAll(".active")]
        // we then use Array.prototype.map() to iterate over the Array
        // of Element Nodes and create a new Array based on those Nodes:
        .map(
          // here we retrueve the value of the data-style custom attribute
          // from the elements, and use that value as the property-name of
          // the cssRules object (defined below), creating an Array of
          // CSS rules:
          (el) => cssRules[el.dataset.style]
        )
    
      // then set the text-content of the <style> element to be a string
      // with each Array element being joined together by a new-line
      // character:
      style.textContent = rules.join("\n");
    
      // a variable to determine if the <style> element has content,
      // if all <button> elements are deselected and none have the
      // 'active' class there will be no content in the Array that
      // was joined; here we test that by assessing whether a the
      // length of the text-content with leading and trailing
      // white-space removed is greater than zero, this returns a
      // Boolean true/false:
      let hasContent = 0 < style.textContent.trim().length;
    
      // here we check to see if the id of the <style> Object is
      // NOT equal to the string, and that the <style> has content:
      if ("user-custom-css-content" !== style.id && hasContent) {
        // if the id doesn't match, we set the id:
        style.id = "user-custom-css-content"
        // and if there is content we append the <style> to the
        // <head> of the document:
        document.head.append(style);
    
        // otherwise if there is no content:
      } else if (false === hasContent) {
        // we use Element.remove() to remove the empty <style>
        // element:
        style.remove();
      }
    }
    
    // a sample of potential CSS rules contained within an Object:
    const cssRules = {
      something: "h1, .item:nth-child(odd) { opacity: 0.5; } figcaption { text-decoration: underline; }",
      blah: "h1 { background: linear-gradient(90deg, transparent, cyan); }",
      test: "img + * { color: rebeccapurple; font-weight: 100;} li:nth-child(even of .item) {border-inline-start: 2px solid fuchsia; padding-inline-start: 1rem; } img { clip-path: polygon(50% 0%, 80% 10%, 100% 35%, 100% 70%, 80% 90%, 50% 100%, 20% 90%, 0% 70%, 0% 35%, 20% 10%);}",
    }
    
    // retrieving a NodeList of <button> elements with the data-style custom
    // attribute:
    const buttons = document.querySelectorAll("button[data-style]")
    
    // iterating over that NodeList - using NodeList.prototype.forEach():
    buttons.forEach(
      // binding the modifyStyles() function as the handler for
      // the 'click' event on the <button> elements:
      (btn) => btn.addEventListener("click", modifyStyles)
    )
    @layer base {
      *,
       ::before,
       ::after {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
      }
      html {
        block-size: 100%;
      }
      body {
        block-size: 100dvh;
        padding-block: 0.5rem;
        padding-inline: 1rem;
      }
      main {
        border: 1px solid;
        inline-size: clamp(20em, 80%, 1000px);
        margin-inline: auto;
        padding: 0.5rem;
      }
      ul,
      ol,
      li {
        list-style-type: none;
      }
      section {
        display: grid;
        gap: 0.5rem;
        grid-template-columns: [full-start text-start] 2fr [text-end fig-start] 1fr [fig-end full-end];
      }
      h1,
      footer {
        grid-column: full;
      }
      .spiel,
      ul {
        grid-column: text;
      }
      ul {
        column-count: 2;
      }
      figure {
        grid-column: fig;
        grid-row: 2 / span 2;
        img {
          clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
          transition: clip-path 2s linear;
        }
      }
      footer {
        .controls {
          display: flex;
          flex-flow: row nowrap;
          gap: 1rem;
          justify-content: space-between;
          &>* {
            flex: 1 0 auto;
          }
          button {
            background: var(--active-color, lightgrey);
            border: 1px solid;
            border-radius: 10rem;
            inline-size: 100%;
            &.active {
              --active-color: lightcyan;
            }
          }
          summary {
            display: block;
            font-size: 1rem;
          }
        }
      }
    }
    <!-- generic HTML laid out as a common card component: -->
    <main>
      <section>
        <h1>Arbitrary title!</h1>
        <p class="spiel">
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquam voluptate totam placeat. Call to action, so on and so forth: do the thing!
        </p>
        <ul>
          <li class="item item-1">List item 1</li>
          <li class="item item-2">List item 2</li>
          <li class="item item-3">List item 3</li>
          <li class="item item-4">List item 4</li>
          <li class="item item-5">List item 5</li>
          <li class="item item-6">List item 6</li>
          <li class="item item-7">List item 7</li>
          <li class="item item-8">List item 8</li>
          <li class="item item-9">List item 9</li>
          <li class="item item-10">List item 10</li>
          <li class="item item-11">List item 11</li>
          <li class="item item-12">List item 12</li>
        </ul>
        <figure>
          <img src="//picsum.photos/id/20/300" alt="" />
          <figcaption>Text related to the featured (demo) image.</figcaption>
        </figure>
        <footer>
          <ul class="controls wrapper">
            <li>
              <!-- using custom data-style attributes to store the name of
                   the cssRules Object property that contains the custom 
                   rules to apply: -->
              <button type="button" id="style-1" class="style-option style-1" data-style="something">
                Style 1
              </button>
            </li>
            <li>
              <button type="button" id="style-2" class="style-option style-2" data-style="blah">
                Style 2
              </button>
            </li>
            <li>
              <button type="button" id="style-3" class="style-option style-3" data-style="test">
                Style 3
              </button>
            </li>
          </ul>
        </footer>
      </section>
    </main>

    JS Fiddle demo.

    References: