loading...

Themeing sites in Lucky

jwoertink profile image Jeremy Woertink ・3 min read

NOTE: this is based on my first round of getting a multi-tenant app to use themes. It's not the cleanest way, but it works for now

Assuming you're using Lucky and you send multiple domains to that same app, you may need to style each site slightly different. To do this, you'd want to theme each site. In these examples, I have a Site model that has a theme column.

The first thing we will do is create a mixin for our actions. In src/actions/mixins/ create a new file themable.cr.

module Themable
  macro included
    expose current_theme
  end

  enum Themes
    Default
    Dark
  end

  # if it returns nil or a non-listed theme
  # just return the default 
  def current_theme : String
    # `current_site` is exposed from a different bit
    t = current_site.theme
    Themes.from_value?(t) ? Themes.from_value?(t).to_s : "Default"
  end
end

Now that we have access to a current_theme method, we just need to include this mixin. In your src/actions/browser_action.cr

abstract class BrowserAction < Lucky::Action
  include Themable
  #...

  macro theme_page
    if current_theme == "Default"
      {{ @type }}Page
    else
      case Themes.parse(current_theme)
      when .dark?
        Dark::{{ @type }}Page
      end
    end
  end
end

In this BrowserAction, I added a macro to help determine which page we are going to render. This will make a bit more sense in the actions.

class Dashboards::Index < BrowserAction
  get "/" do
    render(theme_page)
  end
end

In this root action here, we now render either Dashboards::IndexPage or Dark::Dashboards::IndexPage. This is depending on which current_site has been loaded. As long as you're keeping up with traditional naming conventions, then this just all works.

Now it's time to setup the views portion. In our src/pages/main_layout.cr file, we'll need to update some stuff.

abstract class MainLayout
  include Lucky::HTMLPage

  needs current_theme : String

  abstract def content

  def render
    html_doctype

    html lang: "en" do
      head do
        #... other stuff
        css_link(dynamic_asset("#{@current_theme.downcase}.css"))
      end
      body do
        content
      end
    end
  end
end

For this MainLayout, we are calling either default.css or dark.css. There's a ton of different ways you could handle this. Maybe you have a base.css, and then just inherit and override, or maybe you include a secondary css file? What ever you decide to do with that, just make sure your webpack mix file is updated with all these theme styles.

Another issue you may run in to here is, maybe you have completely different header or footers depending on the theme. You may need to do some additional logic here like:

body do
  if @current_theme == "Dark"
    render_dark_header
  end
  content
end

This of course could be abstracted out to a module in src/components/header_component.cr

module HeaderComponent
  private def render_header(theme)
    case Themable::Themes.parse(theme)
    when .dark?
      _dark_header
    end
  end

  private def _dark_header
    div(class: "dark") do
      h1("Dark Header")
    end
  end
end

abstract class MainLayout
  #...
  include HeaderComponent


  def render
    #...
    body do
      render_header(@current_theme)
      content
    end
  end
end

Lastly, we just need the main view part! For this, we're going to use a special directory structure. We currently have src/pages/dashboards/index_page.cr, and we will just add src/pages/dashboards/dark/index_page.cr. This sort of breaks the hierarchy standard, but for me personally it makes more sense. (for now at least, I'm sure I'll hate it in 3 months lol)

class Dashboards::IndexPage < MainLayout
  def content
    #... home page content here
  end
end

class Dark::Dashboards::IndexPage < ::Dashboards::IndexPage
  def content
    #... dark theme home page content.

    # AND... Bonus! 
    previous_def
    # add additional stuff that's on dark theme and not default
  end
end

Really, that's basically it. Everything else is just going to be your personal preference, or additional things you may need.

If you're reading this, and you can think of some cleaner ways, please comment below!

Discussion

pic
Editor guide