Phoenix 1.7 LiveView Tailwind CSS Website Themer

Phoenix 1.7 LiveView Tailwind CSS Website Themer

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.

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.

📨

Sign Up For Newsletters

Get E-mail updates about our website content & special offers