[A/B Testing] Changing parent CSS class based on experiment variable

AI-generated summary

The discussion revolves around applying separate styles to different templates using the same parent CSS class name (topic-list) for A/B testing with Google Optimize.

daniyal wants to apply different styles to the topic-list class based on a specific variable, and is using Handlebars templates and SCSS to achieve this.

Johani points out that the code const user_id = api.getCurrentUser().id could cause an error for users who are not logged in and suggests an alternative approach. Johani also recommends using a parent selector in SCSS to apply styles to the topic-list class based on a test-theme class added to the body of the HTML document.

Johani proposes adding the test-theme class to the body based on the user’s ID and then using this class in SCSS to apply styles to the topic-list class.

daniyal updates the approach to only modify the topic-list component class by using classNameBindings and setting the test property to true or false based on the user’s ID.

We have an existing theme which we want to experiment. An example would be to experiment different styles of topic list view. For this we are using Google Optimize A/B testing. Currently we plan to show theme without changes to 50% of the users and rest with updated theme. Original and variant should be defined in the same theme as we are going to experiment with small part. The problem is that the parent CSS classes share the same name and we want to apply styles to those parent classes based on specific variable. How can we apply separate styles to different templates as they share the same parent class name.

Below is a simplified scenario of what we are trying to achieve

Consider I have a theme where topic list style is defined like this in common/common.scss

.topic-list {
  tbody {
    border: none;
  }
  tr {
    border: none;
  }
  .example-div {
    border: none;
  }
}

and in my variant, I want to set it as:

.topic-list {
  tbody {
    display: block;
    tr {
      display: block;
    }
  }
  .example-div {
    border: 2px solid black;
  }
  .another-example-div { 
    font-size: 10px;
  }
}

handlebar files which are being overridden; look like this

javascripts/discourse/templates/list/custom-topic-list-item.hbr

<div class=example-div'>
  Hello
</div>

javascripts/discourse/templates/list/variant-custom-topic-list-item.hbr

<div class=example-div'>
  Hello,
  <div class=another-example-div'>
    Nice to meet you
  </div>
</div>

topic-list class name is mentioned in discourse component but structure is like this after render

<table class="topic-list">
  <tr class="topic-list-item">
   <div class=example-div'>
      Hello
    </div>
  </tr>
</table>

I want to render one of these files depending on user ID in my header file
common/header.html

const { findRawTemplate } = require("discourse-common/lib/raw-templates");
const user_id = api.getCurrentUser().id
 
api.modifyClass('component:topic-list-item', {
  renderTopicListItem() {
    let template = findRawTemplate("list/custom-topic-list-item");
    if (user_id % 2 === 0)
       template = findRawTemplate("list/variant-custom-topic-list-item");
    if (template) {
      this.set("topicListItemContents", template(this).htmlSafe());
    }
  },
});

Thanks

Note that this is probably causing an uncaught error to users who are not logged in. I recommend changing that to

const user = api.getCurrentUser();
if (!user) return;

const userId = user.id;

With that out of the way, let’s get back to your question.

Unlike CSS, SCSS supports a parent selector, so that’s something you can use.

Since you want to apply this to every other user (id % 2)

You can check that when the app loads. So, something like this at the very top

const user = api.getCurrentUser();
const userId = user.id;

if (!userId) return;

const testThemeUser = userId % 2 === 0;

if (testThemeUser) {
  document.body.classList.add("test-theme");
}

You can then use the testThemeUser in your JS conditionals and the test-theme CSS class in your styles.

So, something like this.

JS

let template = findRawTemplate("list/custom-topic-list-item");
if (testThemeUser)
  template = findRawTemplate("list/variant-custom-topic-list-item");
if (template) {
  this.set("topicListItemContents", template(this).htmlSafe());
}

SCSS

.topic-list {
  tbody {
    border: none;
  }
  tr {
    border: none;
  }
  .example-div {
    border: none;
  }
  // test theme SCSS
  .test-theme & {
    .example-div {
      border: 2px solid black;
    }
    .another-example-div {
      font-size: 10px;
    }
  }
}

Thanks for your reply @Johani
Currently I only need to update specific component class that is topic-list, so I am using this approach

const useTestTheme = userId % 2 === 0

api.modifyClass('component:topic-list', {
    classNameBindings: ["test"],
    test: useTestTheme,
})