Search code examples
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:

  • Adding CSS rules via CSS syntax (ie, not JavaScript object names, so background-color, not .backgroundColor)
  • Being able to enable and disable the rules
  • Being able to access the rules via a string ID, not a numeric index
  • The rules should be bunched in a single stylesheet object or element since there can be HUNDREDS of packages, so creating a separate stylesheet element for each would be impractical
  • Multiple "packages" can be active at once, it's like a buffet of styles to apply to the page, not just picking a single one
  • This has to work purely through modifying styles, the page itself can't be modified since there are many elements and it's even less practical to add or remove class tags to all of the elements

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: