In this post I demonstrate some of the key features in Phoenix 1.7 LiveView and Tailwind CSS to create a live website theming engine using DaisyUI.
Starting with an Elixir Phoenix 1.7 application, in the root.html.heex we include a partial which includes the HSL color scheme and Tailwind CSS utility library DaisyUI.
<style>
@font-face {
font-family: Geo;
src: url(https://fonts.niccolox.com/Geo.otf);
}
.bg-image-tailwindcss { background-image: url(''); }
<%= if @website.is_theme_customized? do %>
[data-theme=<%= @website.theme %>] {
color-scheme: <%= @website.theme_daisyui_color_scheme %>;
font-family: ;
--p: <%= FolkbotWeb.LayoutView.convert_to_HSL(@website.theme_daisyui_primary_color) %>; /* primary required */
--s: <%= @website_theme_secondary_color_hsl %>;
--a: <%= @website_theme_accent_color_hsl %>;
--n: <%= @website_theme_neutral_color_hsl %>;
--b1: <%= @website_theme_base_100_color_hsl %>;;
--in: <%= @website_theme_info_color_hsl %>;
--su: <%= @website_theme_success_color_hsl %>;
--wa: <%= @website_theme_warning_color_hsl %>;
--er: <%= @website_theme_error_color_hsl %>;
--animation-btn: <%= @website.theme_daisyui_animation_btn %>;
--animation-input: <%= @website.theme_daisyui_animation_input %>;
--border-color: <%= @website.theme_daisyui_border_color %>;
--btn-text-case: <%= @website.theme_daisyui_btn_text_case %>;
--btn-focus-scale: <%= @website.theme_daisyui_btn_focus_scale %>;
--border-btn: <%= @website.theme_daisyui_border_btn %>;
--navbar-padding: <%= @website.theme_daisyui_navbar_padding %>;
--rounded-box: <%= @website.theme_daisyui_rounded_box %>;
--rounded-btn: <%= @website.theme_daisyui_rounded_btn %>;
--rounded-badge: <%= @website.theme_daisyui_rounded_badge %>;
--tab-border: <%= @website.theme_daisyui_tab_border %>;
--tab-radius: <%= @website.theme_daisyui_tab_radius %>;
}
<% else %>
<% end %>
</style>
Our website.ex schema has the various Daisy and TailwindCSS utility CSS classes.
import EctoEnum
defenum WebsiteTypes, ["normal", "custom", "landing"]
defenum(WebsiteThemes,
light: 0,
dark: 1,
cupcake: 2,
bumblebee: 3,
emerald: 4,
corporate: 5,
synthwave: 6,
retro: 7,
cyberpunk: 8,
valentine: 9,
halloween: 10,
garden: 11,
forest: 12,
aqua: 13,
lofi: 14,
pastel: 15,
fantasy: 16,
wireframe: 17,
black: 18,
luxury: 19,
dracula: 20,
cmyk: 21,
eighties: 22,
folkbot: 23,
gray: 24,
autumn: 25,
business: 26,
acid: 27,
lemonade: 28,
customtheme: 29,
snow: 30,
rwb: 31,
punk: 32,
oldpunk: 33,
customdaisy: 34
)
defenum ThemeBackgrounds,
[
"",
"from-secondary to-primary text-primary-content flex flex-col items-left bg-gradient-to-br",
"from-primary to-secondary text-primary-content flex flex-col items-left bg-gradient-to-br",
"from-primary to-secondary text-primary-content -mt-[4rem] flex flex-col items-center bg-gradient-to-br pt-32 pb-48",
"bg-base-100",
"bg-base-200",
"bg-base-300",
"bg-black",
"bg-white",
"bg-primary text-primary-content flex flex-col items-left",
"bg-primary",
"bg-secondary",
"bg-accent",
]
defenum ThemeHomePageTemplate,
[
"index",
"index_animated",
"index_landing",
"index_blog_grid_tall_landing",
"index_posts_featured_hero",
"index_person",
"index_hero_info",
"index_landing_foundation",
"index_landing_fettle",
"index_landing_outlet",
"index_landing_republic_dev",
"index_landing_masonry",
"index_landing_book",
"index_hero_big_face",
"index_landing_tributary",
"index_landing_essential",
"index_landing_impulse",
"index_landing_launch"
]
defenum OrganizingColor,
[
"primary",
"secondary",
"neutral",
"accent",
"info",
"success",
"warning",
"error"
]
defenum ColorScheme,
[
"dark",
"light"
]
defenum ButtonCase,
[
"uppercase",
"lowercase"
]
defenum FontFamily,
[
"font-sans",
"font-serif",
"font-mono",
"font-display",
"font-tangerine"
]
defenum FontFamilyHeader,
[
"sans",
"serif",
"mono",
"display",
"crimson",
"tangerine",
"Geo"
]
defmodule Folkbot.Websites.Website do
use Ecto.Schema
use Folkbot.Schema
import Ecto.Changeset
alias Folkbot.Channels.Channel
alias Folkbot.Accounts.User
alias Folkbot.Websites.Nginx
use Waffle.Ecto.Schema
schema "websites" do
field :name, :string
field :domain, :string
field :slug, :string
field :about, :string
field :address, :string
field :banner, Folkbot.WebsiteImageUploader.Type
field :contact, :string
field :description, :string
field :disclaimer, :string
field :footer, :string
field :type, WebsiteTypes
field :icon, Folkbot.WebsiteImageUploader.Type
field :layout, :string
field :meta_description, :string
field :privacy, :string
field :primary_color, :string
field :secondary_color, :string
field :status, :string
field :tagline, :string
field :terms, :string
field :theme, :string
field :is_theme_customized?, :boolean
field :theme_font_family_header, :string
field :theme_daisyui_primary_color, :string
field :theme_daisyui_secondary_color, :string
field :theme_daisyui_accent_color, :string
field :theme_daisyui_neutral_color, :string
field :theme_daisyui_base_100_color, :string
field :theme_daisyui_base_200_color, :string
field :theme_daisyui_base_300_color, :string
field :theme_daisyui_info_color, :string
field :theme_daisyui_success_color, :string
field :theme_daisyui_warning_color, :string
field :theme_daisyui_error_color, :string
field :theme_daisyui_header_organizing_color, OrganizingColor, default: "primary"
field :theme_daisyui_footer_organizing_color, OrganizingColor, default: "secondary"
field :theme_daisyui_color_scheme, ColorScheme, default: "light"
field :theme_daisyui_font_family, FontFamily, default: "font-sans"
field :theme_daisyui_animation_btn, :string
field :theme_daisyui_animation_input, :string
field :theme_daisyui_border_color, :string
field :theme_daisyui_btn_text_case, ButtonCase, default: "uppercase"
field :theme_daisyui_btn_focus_scale, :string
field :theme_daisyui_border_btn, :string
field :theme_daisyui_navbar_padding, :string
field :theme_daisyui_rounded_box, :string
field :theme_daisyui_rounded_btn, :string
field :theme_daisyui_rounded_badge, :string
field :theme_daisyui_tab_border, :string
field :theme_daisyui_tab_radius, :string
field :theme_background, ThemeBackgrounds
field :theme_background_image, Folkbot.WebsiteImageUploader.Type
field :theme_homepage_template, ThemeHomePageTemplate, default: "index"
field :is_free?, :boolean, default: false
field :is_published?, :boolean, default: false
field :is_listed?, :boolean, default: false
field :is_featured?, :boolean, default: false
field :is_private?, :boolean, default: false
field :is_invitable?, :boolean, default: false
field :is_previewable?, :boolean, default: false
field :is_premium?, :boolean, default: false
field :creator_id, :id
belongs_to :user, User
belongs_to :owner, User
field :contact_id, :id
has_many :channels, Channel, on_delete: :nothing
timestamps()
end
@doc false
def changeset(website, attrs) do
website
|> cast(attrs,
[
:name,
:slug,
:tagline,
:description,
:meta_description,
:about,
:contact,
:address,
:terms,
:disclaimer,
:privacy,
:footer,
:primary_color,
:secondary_color,
:theme,
:theme_background,
:is_theme_customized?,
:theme_homepage_template,
:theme_font_family_header,
:theme_daisyui_primary_color,
:theme_daisyui_secondary_color,
:theme_daisyui_accent_color,
:theme_daisyui_neutral_color,
:theme_daisyui_base_100_color,
:theme_daisyui_base_200_color,
:theme_daisyui_base_300_color,
:theme_daisyui_info_color,
:theme_daisyui_success_color,
:theme_daisyui_warning_color,
:theme_daisyui_error_color,
:theme_daisyui_header_organizing_color,
:theme_daisyui_footer_organizing_color,
:theme_daisyui_warning_color,
:theme_daisyui_color_scheme,
:theme_daisyui_font_family,
:theme_daisyui_animation_btn,
:theme_daisyui_animation_input,
:theme_daisyui_border_color,
:theme_daisyui_btn_text_case,
:theme_daisyui_btn_focus_scale,
:theme_daisyui_border_btn,
:theme_daisyui_navbar_padding,
:theme_daisyui_rounded_box,
:theme_daisyui_rounded_btn,
:theme_daisyui_rounded_badge,
:theme_daisyui_tab_border,
:theme_daisyui_tab_radius,
:layout,
:domain,
:status,
:owner_id,
:creator_id,
:contact_id,
:user_id,
:type,
:is_published?,
:is_listed?,
:is_featured?,
:is_private?,
:is_invitable?,
:is_previewable?,
:is_premium?,
:is_free?
])
# |> foreign_key_constraint(:owner_id)
|> cast_attachments(attrs, [:icon, :banner, :theme_background_image])
|> validate_required([:name, :domain])
|> slugify_domain()
# |> write_nginx_conf()
end
defp slugify_domain(changeset) do
if domain = get_change(changeset, :domain) do
domainatrex = Domainatrex.parse(domain) |> elem(1) |> Map.fetch!(:domain)
put_change(changeset, :slug, slugify(domainatrex))
else
changeset
end
end
defp slugify(str) do
str
|> String.downcase()
|> String.replace(~r/[^\w-]+/, "-")
end
end
The LiveView Phoenix form component creates the LiveView which has a side-by-side with a demo mobile viewport.
form_component.ex
defmodule FolkbotWeb.WebsiteLive.FormComponent do
use FolkbotWeb, :live_component
alias Folkbot.Websites
@impl true
def render(assigns) do
~H"""
<div>
<.header>
<%= @title %>
<:subtitle>Use this form to manage website records in your database.</:subtitle>
</.header>
<.simple_form
for={@form}
id="website-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save"
>
<.input field={@form[:theme]} type="select" options={FolkbotWeb.WebsiteView.theme_options()} label="Theme" />
<.input field={@form[:theme_daisyui_header_organizing_color]} type="select" options={FolkbotWeb.WebsiteView.organizing_color_options()} label="Theme Header Color" />
<.input field={@form[:theme_daisyui_footer_organizing_color]} type="select" options={FolkbotWeb.WebsiteView.organizing_color_options()} label="Theme Footer Color" />
<.input field={@form[:is_theme_customized?]} type="checkbox" label="Is theme customized?" />
<.input field={@form[:theme_daisyui_primary_color]} type="color" label="Theme daisyui primary color" />
<.input field={@form[:theme_daisyui_border_color]} type="color" label="border color" />
<.input field={@form[:theme_daisyui_border_btn]} type="text" label="border button width" />
<.input field={@form[:theme_daisyui_secondary_color]} type="color" label="Theme daisyui secondary color" />
<.input field={@form[:theme_daisyui_accent_color]} type="color" label="Theme daisyui accent color" />
<.input field={@form[:theme_daisyui_neutral_color]} type="color" label="Theme daisyui neutral color" />
<.input field={@form[:theme_daisyui_base_100_color]} type="color" label="Theme daisyui base 100 color" />
<.input field={@form[:theme_daisyui_info_color]} type="color" label="Theme daisyui info color" />
<.input field={@form[:theme_daisyui_success_color]} type="color" label="Theme daisyui success color" />
<.input field={@form[:theme_daisyui_warning_color]} type="color" label="Theme daisyui warning color" />
<.input field={@form[:theme_daisyui_error_color]} type="color" label="Theme daisyui error color" />
<.input field={@form[:theme_background]} type="select" options={FolkbotWeb.WebsiteView.theme_background_options()} label="Theme background" />
<.input field={@form[:theme_homepage_template]} type="select" options={FolkbotWeb.WebsiteView.theme_homepage_template_options()} label="Theme homepage template" />
<.input field={@form[:theme_daisyui_btn_text_case]} type="select" options={FolkbotWeb.WebsiteView.button_case_options()} label="Button text case" />
<.input field={@form[:theme_daisyui_font_family]} type="select" options={FolkbotWeb.WebsiteView.font_family_options()} label="Font family" />
<.input field={@form[:theme_font_family_header]} type="select" options={FolkbotWeb.WebsiteView.theme_font_family_header_options()} label="Font family header" />
<:actions>
<.button phx-disable-with="Saving...">Save Website</.button>
</:actions>
</.simple_form>
</div>
"""
end
@impl true
def update(%{website: website} = assigns, socket) do
changeset = Websites.change_website(website)
{:ok,
socket
|> assign(assigns)
|> assign_form(changeset)}
end
@impl true
def handle_event("validate", %{"website" => website_params}, socket) do
changeset =
socket.assigns.website
|> Websites.change_website(website_params)
|> Map.put(:action, :validate)
{:noreply, assign_form(socket, changeset)}
end
def handle_event("save", %{"website" => website_params}, socket) do
save_website(socket, socket.assigns.action, website_params)
end
defp save_website(socket, :edit, website_params) do
case Websites.update_website(socket.assigns.website, website_params) do
{:ok, website} ->
notify_parent({:saved, website})
{:noreply,
socket
|> put_flash(:info, "Website updated successfully")
|> push_patch(to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign_form(socket, changeset)}
end
end
defp save_website(socket, :new, website_params) do
case Websites.create_website(website_params) do
{:ok, website} ->
notify_parent({:saved, website})
{:noreply,
socket
|> put_flash(:info, "Website created successfully")
|> push_patch(to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign_form(socket, changeset)}
end
end
defp assign_form(socket, %Ecto.Changeset{} = changeset) do
assign(socket, :form, to_form(changeset))
end
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
end
Nicholas Roberts is a creative tech consultant Hello there, I am open to Elixir Phoenix projects as a full-stack engineer, cross-functional project manager or product development lead. I am an Australian-USA dual national and am available for remote or hybrid work in California.
- 20 years web, media and tech
- 5 years Phoenix Elixir
- Contact https://www.linkedin.com/in/niccolo/
- Email [email protected]
- Phone 510 684 8264