genbatro
genbatro

Reputation: 166

Efficiently apply style with JavaScript (best practice?)

I am writing a userscript for Greasemonkey/Tampermonkey (and learning JS in the process). It is for a discussion forum (/bulletin board), where each post is assigned one of six possible styles based on some criteria. The post contains a div with the username, a div with a timestamp, a div with avatar and user info, and a div for the content itself (which may or may not include a quote div).

For simplicity's sake, let's just call the styles red, blue, green, yellow, black and white. (Note: it is not just color – each style has its own distinct value of "everything", even margins, paddings and other layout.)

Then, each post is styled with a call to a style-changing function, for example makeRed(post) or makeGreen(post).

Then, each of those functions look something like this:

const makeRed = post => {
    let author = post.querySelector(".author");
    author.style.fontSize = "...";
    author.style.fontFamily = "...";
    author.style.backgroundColor = "...";
    author.style.padding = "...";
    author.style.borderRadius = "...";
    author.style.margin = "...";
    author.style.flex = "...";
    // ...

    let date = post.querySelector(".date");
    date.style.fontFamily = "...";
    date.style.fontSize = "...";
    date.style.color = "...";
    date.style.margin = "...";
    date.style.flex = "...";
    // ...

    let avatarInfo = post.querySelector(".infoBox");
    avatarInfo.style.backgroundColor = "...";
    avatarInfo.style.fontSize = "...";
    // ...

    let content = post.querySelector(".content");
    content.style.borderColor = "...";
    content.style.backgroundImage = "...";
    // ...

    let quote = content.querySelector(".quote");
    if (quote) {
        // Lots of quote styling here
    } else {
        // Lots of quote-less styling here
    }
}

Each of these functions contain significantly more lines of code in a similar fashion, I just cut them out of this question to save some space.

So, to the question:
Is there any way to write this more concisely/elegantly?

I guess it's hard to avoid having a line for each style property, but at least in CSS it looks a bit nicer, IMO. Would it be a better practice to create a CSS-file and somehow import it with JavaScript (how?), or is this long list of element.style.property = "..." actually the way to go? Or is there a better way? How would you do it?

Also, I'm brand new to JS, so if you have any other advice (related to my code or not), please let me know!
Thank you very much! :-)

Edit:
I was asked to include the HTML structure. A typical post is simply something like this:

<div class="post">
  <div class="author">Author McAuthorson</div>
  <div class="date">2021.01.10 01:23</div>
  <div class="infoBox">
    <div class="avatar"><img src="..." /></div>
    <div class="userinfo">User info here</div>
  </div>
  <div class="content">
    <div class="quote">Some quote by some guy here</div>
    Some original text by McAuthorson here.
  </div>   
</div>

Upvotes: 5

Views: 1831

Answers (4)

KooiInc
KooiInc

Reputation: 122898

You can use a helper function.

[2023] See this Stackblitz snippet.

changeStyle(document.querySelector(".sample"), {
  fontSize: "12px",
  fontFamily: "verdana, arial",
  lineHeight: "15px",
  fontWeight: "bold",
  textAlign: "center",
  backgroundColor: "#fffff0",
  color: "green",
  height: "1.2rem"
});

function changeStyle(forElement, props) {
  Object.entries(props).forEach( ([key, value]) => forElement.style[key] = value);
}
<div class="sample">Sample</div>

Or create/modify css within an injected custom css stylesheet

const wait = 5;
// initial styling 
changeRuleset("body", {
  font: "12px/15px 'verdana', 'arial'", 
  margin: "2rem" });
changeRuleset("pre", { 
  whiteSpace: "pre-line", 
  maxWidth: "800px", 
  marginTop: "2rem"});
changeRuleset(".sample", { 
  fontSize: "1.5rem", 
  fontFamily: "comic sans MS", color: "green" });
changeRuleset("[data-cntr]:before", {content: `"[" attr(data-cntr) "] "`});
//                        ^ pseudo class allowed

displaySheet(getOrCreateCustomStyleSheet(), "Initial");

document.querySelector(".sample").dataset.cntr = wait;
timer();

setTimeout(() => {
  // changed styling
  changeRuleset(".sample", {
    fontSize: "1.5rem", // camelCase or ...
    "background-color": "yellow", // hyphen ... both allowed
    "box-shadow": "2px 2px 8px #999",
    fontWeight: "bold",
    display: "inline-block",
    padding: "20px",
    color: "red"
  });
  changeRuleset(".sample:after", { content: "'!'" });
  displaySheet(getOrCreateCustomStyleSheet(), "Changed"); }, wait * 1000);

// change a ruleset in the custom style sheet
function changeRuleset(someRule, styleProps) {
  const customSheet = getOrCreateCustomStyleSheet();
  const formatKey = key => key.replace(/[A-Z]/g, a => `-${a.toLowerCase()}`);
  //                       ^ remove camelCase, apply hyphen
  const compareKeys = (key1, key2) =>
    key1.replace(/:/g, "") === formatKey(key2).replace(/:/g, "");
  //              ^ comparison for pseudoClasses
  let rulesStr = styleProps ?
    Object.entries(styleProps).reduce((acc, [key, value]) =>
      [...acc, `${formatKey(key)}: ${value};`]
      , []).join("\n") : "";
  const existingRuleIndex = customSheet.rules.length &&
    [...customSheet.rules]
      .findIndex(r =>
        r.selectorText && compareKeys(r.selectorText, someRule));

  if (existingRuleIndex > -1) {
    // replace existing rule
    customSheet.rules.length && customSheet.deleteRule(existingRuleIndex);
    customSheet.insertRule(`${someRule} { ${rulesStr} }`, existingRuleIndex);
  } else {
    // insert new rule
    customSheet.insertRule(`${someRule} { ${rulesStr} }`);
  }
}

// get or create and get the custom style sheet
function getOrCreateCustomStyleSheet() {
  let styleTrial = document.querySelector("#customCss");

  // create style block if it does not exists
  if (!styleTrial) {
    document.querySelector("head").appendChild(
      Object.assign(
        document.createElement("style"), {
          type: "text/css",
          id: "customCss",
          title: "Custom styles"
      })
    );
    styleTrial = document.querySelector("#customCss");
  }

  return styleTrial.sheet;
}

// for demo
function displaySheet(sheet, term) {
  document.querySelector("pre").textContent += `
  --------------------------------------
  **${term} rules in style#customCss**
  --------------------------------------
  ${
    [...sheet.rules].reduce( (acc, val) => 
      [ ...acc, `${val.cssText}\n`  ], []).join("\n")}`;
}

function timer() {
  setTimeout(() => {
    const cntrEl = document.querySelector("[data-cntr]");
    const current = cntrEl.dataset.cntr - 1;
    cntrEl.dataset.cntr = current > 0 ? current : "";
    return current > 0 
      ? timer() 
      : changeRuleset("[data-cntr]:before", {content: ""});
  }, 1000);
}
<div class="sample">Hello World</div>
<pre></pre>

Upvotes: 2

alexjkni.dev
alexjkni.dev

Reputation: 35

Seems your quite new to JS. Basically, manipulating CSS values using JS isn't recommended unless the entire solution relies on the CSS values being dynamic alongside the JS (and even then, there's much more elegant, efficient solutions).

When working with JS, you ALWAYS need to consider how long things will actually take to run & what conflicts it could cause down the line. With your method, you're looking at some heavy future development work if you run into issues down the line (like you are now) and have to change something OR if something else you action conflicts with this.

As others have said, just utilize CSS (or SCSS) for this. The most simplistic way would be to define 6 unique CSS classes and nth number of shared classes between the elements if required. Then when a post is submitted on your bulletin board, the BACKEND code assigns the post with a class name (or ID, or something else that says this post means 'post-style__variant-1') that is relative to your class name (and can be relative to other things down the line).

Extra tip is to consider VARIANTS. Basically your bulletin post is static but that bulletin board post can have 6 different styling, thus at this point your bulletin board posts can have 6 variants based on the 6 different class names. If you build your posts submission system with variants in mind, you'll be able to extend it in the future. For the CSS/SCSS structure, you can just do the below

CSS

.post__variant-1 {
   color: red;
}

.post__variant-2 {
   color: green;
}

.post__variant-3 {
   color: blue;
}

^ Not recommended by the way, horrible to write and horrible to maintain. Judging by what you're learning, I'd recommend getting into SCSS as soon as possible. It'll make your life a lot easier. For example

.post__variant {

    display: block;

    &-1 {
        @extend .post__variant;

        color: red;
    }

    &-2 {
        @extend .post__variant;

        color: green;
    }


    &-3 {
        @extend .post__variant;

        color: blue;
    }
}

SCSS is a preprocessor by the way. You code the SCSS locally, then run a command which takes the SCSS files, runs them through the preprocessor which then spits out the SCSS as CSS. So everything gets rewritten for you into the language the browser understands. So the SCSS above would preprocess into

.post__variant, .post__variant-3, .post__variant-2, .post__variant-1 {
  display: block;
}
.post__variant-1 {
  color: red;
}
.post__variant-2 {
  color: green;
}
.post__variant-3 {
  color: blue;
}

Hopefully that all makes sense. I used to delve into bulletin board development when I started out, it's a good & pretty standard entry point. Additionally, ignore anyone calling you out (or negatively criticizing you) for going with a solution you thought up. Your solution might not be the best but developers are ALWAYS learning. As soon as you think you're the best and there's nothing else to learn. Congratulations, you've become a basement developer that'll be used to work on shitty legacy projects and ignored by whatever company you decide to work with. Only kept around to maintain the shite legacy stuff.

Upvotes: 2

ubaid shaikh
ubaid shaikh

Reputation: 453

You can create CSS dynamically and inject it as follows:

function addStyle(styleString) {
    const style = document.createElement('style');
    style.textContent = styleString;
    document.head.append(style);
}

addStyle(`
    .red .author {
        font-size: 12px;
        font-family: 'Times New Roman', Times, serif;
        background-color: red;
        padding: 10px;
        border-radius: 5px;
        margin: 10px;
        flex: 1;
    }

    .red .date{
        font-family: "...";
        font-size: "...";
        color: "...";
        margin: "...";
        flex: "...";
    }

    .red ...{
        ...
    }
    /* similarly for other colors */

    .blue {
        color: blue;
    }
`);

function removeStyles(post) {
    //remove all the colors
    post.classList.remove('red', 'green', 'blue', ...);
}

const makeRed = post => {
    removeStyles(post);
    post.classList.add('red');
}

const makeGreen = post => {
    removeStyles(post);
    post.classList.add('green');
}
<div class="post">
        <div class="author">Author McAuthorson</div>
        <div class="date">2021.01.10 01:23</div>
        <div class="infoBox">
            <div class="avatar"><img src="..." /></div>
            <div class="userinfo">User info here</div>
        </div>
        <div class="content">
            <div class="quote">Some quote by some guy here</div>
            Some original text by McAuthorson here.
        </div>
    </div>

Upvotes: 2

Tibebes. M
Tibebes. M

Reputation: 7538

One approach would be to use CSS injection and add/remove classes to the elements you wish to alter.

Here is an example

const makeRed = post => post.classList.add('red')

const css = `
 .red > .author {
    background: red;
    font-weight: bold;
 }
 .red > .date {
    font-weight: bold;
 }
`

// inject the style first
document.head.insertAdjacentHTML("beforeend", `<style>${css}</style>`)

// call the func. to add the class then
setTimeout(() => makeRed(document.querySelector("#test")), 1000)
<div class="post" id="test">
  <div class="author">Author McAuthorson</div>
  <div class="date">2021.01.10 01:23</div>
  <div class="infoBox">
    <div class="avatar"><img src="..." /></div>
    <div class="userinfo">User info here</div>
  </div>
  <div class="content">
    <div class="quote">Some quote by some guy here</div>
    Some original text by McAuthorson here.
  </div>
</div>

Upvotes: 3

Related Questions