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!