How to Serve Multiple Themes in Rails Using Sass and esbuild


In this article, I’ll show you one way to create multiple themes using a server-side variable that automatically renders a theme in Rails (with esbuild).

What’s the use case?

  • Your page has a parent resource that dictates your app’s branding
  • You don’t want spaghetti javascript to handle themes.

Before jumping in, let’s understand how the compilation works
Rails has its own default asset pipeline (Sprockets). Its job is to pre-process, concatenate, minify, and serve these files (goes inside public/assets folder). Shifting to esbuild or webpack turns the approach to be more modern (easy to manage dependencies separately from the back-end), faster (written in lower level languages like Go) and flexible (we can configure each step of the process the way we want).


For our approach we will create structure like this:

  • 1 Sass file with all your classes and common imports
  • 1 Sass file with common variables/mixins
  • 1 Sass file per theme with the variables you want to customize
  • 1 method to control the theme to be rendered



Stylesheets

Starting with the common classes and imports, you need to create a partial, meaning, a file starting with an underscore(_). Give it a generic name like styles or global.

// app/assets/stylesheets/_global.scss 
@import "bootstrap/scss/bootstrap";
// your other imports

// your classes
#some-id{
  background: $primary;
}

.some-class {
  background-color: $primary;
}
Enter fullscreen mode

Exit fullscreen mode

Then we can create a Sass partial to accommodate common variables which will be needed across all themes.

// app/assets/stylesheets/_common_variables.scss

//
// Color system
//

// GRAY SCALE
$white:  #fff !default;
$gray-100: #d5efd5 !default;
$gray-200: #e9ecef !default;
$gray-300: #dee2e6 !default;
$gray-400: #ced4da !default;
$gray-500: #adb5bd !default;
$gray-600: #85ba91 !default;
$gray-700: #a8dac4 !default;
$gray-800: #343a40 !default;
$gray-900: #212529 !default;
$black:  #000 !default;

// DEFAULT COLORS
$blue:    #0d6efd;
$indigo:  #6610f2;
$purple:  #6f42c1;
$pink:    #d63384;
$red:     #dc3545;
$orange:  #fd7e14;
$yellow:  #ffc107;
$green:   #198754;
$teal:    #20c997;
$cyan:    #0dcaf0;
$light-red: #f6d4d6;

// SYSTEM COLORS
$primary: #0C1439;
$dark: #0C1439;
$secondary:     $gray-600;
$success:       #00B554;
$info:          #5D97D1;
$warning:       #DB7000;
$danger:        #D5113B;
$light:         $gray-100;

$primary-50: #0C143905 !default;
$primary-100: #0C143910 !default;
$primary-200: #0C143920 !default;
$primary-300: #0C143930 !default;
$primary-400: #0C143940 !default;
$primary-500: #0C143950 !default;
$primary-600: #0C143960 !default;
$primary-700: #0C143970 !default;
$primary-800: #0C143980 !default;
$primary-900: #0C143990 !default;

Enter fullscreen mode

Exit fullscreen mode

Now, for our theme files, we want to follow a structure like this:

// import common variables

// define theme variables

// import styles
Enter fullscreen mode

Exit fullscreen mode

Why do we need to split this way?
All these styles need to be precompiled, meaning when you serve the application your SCSS turns into a single, ready-to-use compiled CSS file (the browser doesn’t understand Sass). While I’m using bootstrap for this example, override variables need to be defined before importing it. As well, before our classes. Each theme file will need to have ALL definitions. This might sound redundant, but in reality it just allows the system to serve one single file, instead of all of them, which makes it a bit more performant. Adding to the fact that being precompiled we can’t change it dynamically. Any other way would be to create duplicated classes and add a prefix of the theme, but in my opinion that sounds worse. Because you would need to compute for each HTML element what theme you would like to use.

Usually you start your project with an application.scss aggregating all your styles for your app.
In this case, our application will be the default theme to be precompiled.

The default theme:

// app/assets/stylesheets/application.scss
//
// Start Variables
//
@import "common_variables";

$primary: #0C1439;
$dark: #0C1439;
$secondary: $gray-600;
$success: #00B554;
$info: #5D97D1;
$warning:  #DB7000;
$danger:  #D5113B;
$light: $gray-100;

//
// End Variables
//

@import "global";
Enter fullscreen mode

Exit fullscreen mode

One theme, let’s call it halloween:

// app/assets/stylesheets/halloween.scss

//
// Start Variables
//
@import "common_variables";

$primary: #d5810e;
$dark: #292929;
$secondary: #590606;
$success: #d36546;
$info: #cfc2a1;
$warning: #d3804c;
$danger: #9A0539FF;
$light: #dac3a8;

//
// End Variables
//

@import "global";
Enter fullscreen mode

Exit fullscreen mode

Another theme and let’s call it xmas:

// app/assets/stylesheets/xmas.scss

//
// Start Variables
//
@import "common_variables";

$primary: #b80606;
$dark: #0d3903;
$secondary: #d3d3d3;
$success: #5da104;
$info: #87b6b6;
$warning: #dd4b2a;
$danger: #700909;
$light: #ffffbe;

//
// End Variables
//

@import "global";

Enter fullscreen mode

Exit fullscreen mode



Compilation

This alone doesn’t do anything.

When you decide to use Sass in your styles, you will need to compile them into a single .css files, which will be served and read in the browser.

Using esbuild, we need to map each SCSS file to its corresponding compiled CSS file.

You need to make sure you have sass installed in your packages.

// 1
"build:css": "sass ./app/assets/stylesheets/application.scss:./app/assets/builds/application.css ./app/assets/stylesheets/halloween.scss:./app/assets/builds/halloween.css ./app/assets/stylesheets/xmas.scss:./app/assets/builds/xmas.css --no-source-map --load-path=node_modules",

// 2
"build:css:watch": "nodemon --watch ./app/assets/stylesheets/ --ext scss --exec \"yarn build:css\""
Enter fullscreen mode

Exit fullscreen mode

  1. Map your scss files into the expected css files
  2. A command (using nodemon, you’ll need to add it as well) to watch for changes and compile them

Compile it using yarn build:css.

To see it in action, you can change manually the name of the stylesheet in your main layout, by replacing “application” with “xmas” or “halloween”.


  
  <%= stylesheet_link_tag "xmas", "data-turbo-track": "reload" %>
  

Enter fullscreen mode

Exit fullscreen mode

If your styles aren’t compiling in development, make sure you don’t have any ‘builds‘ or ‘assets‘ inside public/. Rails prioritises this folder when you precompile using Rails engine instead of esbuild. You can read more about here it has a small explanation at the end of the section.



Theme selector

Next, we need to automatically swap the theme based on some logic. In this case, I created a resource where I’m expecting a :party parameter and I will use it to determine which theme I want to serve.

# routes.rb
resources :parties, only: [ :show ], param: :party, path: "https://dev.to/"
Enter fullscreen mode

Exit fullscreen mode

In our ApplicationController or any other base controller, you need to create a function that looks at this parameter and determines the correct theme.

# application_controller.rb
helper_method :current_theme

# this is not private
def current_theme
  party = params[:party].to_s
  %w[xmas halloween].include?(party) ? party : "application"
end
Enter fullscreen mode

Exit fullscreen mode

By using helper_method we’re allowing this method to be called from the view.

Basically, we are expecting a parameter that matches the name of one of our stylesheet files; if it doesn’t, we’ll render our default theme.

Now, we just need to call this function in our main layout application.html.erb, by replacing the string that defines the stylesheet we want to use.

Instead of a url parameter, we could eventually have a button in the UI, save the selection in the session and read from it in the helper_method we just created.


  
  <%= stylesheet_link_tag current_theme, "data-turbo-track": "reload" %>
 

Enter fullscreen mode

Exit fullscreen mode

If you look at your Network tab in the browser’s inspector, you will see that only one file is downloaded and not all of them.

Showcase of multiple teams in a gif

Happy Coding šŸ™‚



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *