This presentation was built with Phlex

class PhlexPresentation < Presentation
  def self.presentation_title = "Build Phlex Rails Applications"

  def slides
    [

      TitleSlide(
        title: title,
        subtitle: "Component-driven front-end development",
        class: "bg-gradient-to-tr from-violet-600 to-indigo-600 text-white"
      ),

      TitleSlide(
        title: "WARNING",
        subtitle: "What works for me might not work best for you.",
        class: "bg-red-600 text-white"
      ),

      TitleSlide(
        title: "WARNING",
        subtitle: %{First look at Phlex and the thought is usually, "that's a terrible idea" — It's like Tailwind; you just gotta try it.},
        class: "bg-red-700 text-white"
      ),

      TitleSlide(
        title: "WARNING",
        subtitle: %{After you try it, half of you will love it. The other half of you will probably hate it.},
        class: "bg-red-800 text-white"
      ),

      TitleSlide(
        title: "WARNING",
        subtitle: %{I did a stupid thing and created this presentation software while creating this presentation.},
        class: "bg-red-900 text-white"
      ),

      TitleSlide(
        title: "Phlex",
        class: "bg-blue-700 text-white",
      ){
        Title(class: "font-serif") {
          span { @title }
          whitespace
          span(class: "font-light italic") { "/fleks/" }
        }
        Subtitle(class: "font-serif") { "Phlex is a Ruby gem for building fast object-oriented HTML and SVG components. Views are described using Ruby constructs: methods, keyword arguments and blocks, which directly correspond to the output." }
        Subtitle(class: "font-serif") {
          plain "Created by "
          a(href: "https://www.namingthings.org", class: "underline"){ "Joel Drapper" }
          plain "." }
      },

      ContentSlide(
        title: "This is a Phlex component",
        subtitle: "Phlex is a plain 'ol Ruby object that can render HTML. Check out this navigation menu implemented in Phlex."
      ){
        TwoUp {
          VStack {
            Code(:ruby, title: "Here's Phlex"){
              <<~RUBY
                class Nav < Phlex::HTML
                  def view_template
                    nav(class: "main-nav") {
                      ul {
                        li { a(href: "/") { "Home" } }
                        li { a(href: "/about") { "About" } }
                        li { a(href: "/contact") { "Contact" } }
                      }
                    }
                  end
                end
              RUBY
            }
          }

          VStack {
            Code(:html, title: "Here's what it renders"){
              <<~HTML
                <nav class="main-nav">
                  <ul>
                    <li><a href="/">Home</a></li>
                    <li><a href="/about">About</a></li>
                    <li><a href="/contact">Contact</a></li>
                  </ul>
                </nav>
              HTML
            }
          }
        }
      },

      ContentSlide(
        title: "Slots are blocks",
        subtitle: "The `item` method accepts a block, which is rendered in the navigation `li > a` tag."
      ){
        TwoUp {
          Code(:ruby, title: "Navigation component implementation") {
            <<~RUBY
              class Nav < Phlex::HTML
                def view_template(&content)
                  nav(class: "main-nav") { ul(&content) }
                end

                def item(url, &content)
                  li { a(href: url, &content) }
                end
              end
            RUBY
          }

          Code(:ruby, title: "Calling the navigation component"){
            <<~RUBY
              render Nav.new do |it|
                it.item("/") { "Home" }
                it.item("/about") { "About" }
                it.item("/contact") { "Contact" }
              end
            RUBY
          }
        }
      },

      ContentSlide(
        title: "Extend components with inheritance",
        subtitle: "Useful for shipping component libraries, prototyping new features, or for page layouts."
      ){
        TwoUp {
          Code(:ruby, title: "Tailwind component") {
            <<~RUBY
              class TailwindNav < Nav
                def view_template(&content)
                  nav(class: "flex flex-row gap-4", &content)
                end

                def item(url, &content)
                  a(href: url, class: "text-underline", &content)
                end
              end
            RUBY
          }

          VStack {
            Code(:ruby, title: "Calling the navigation component"){
              <<~RUBY
                render TailwindNav.new do |it|
                  it.item("/") { "Home" }
                  it.item("/about") { "About" }
                  it.item("/contact") { "Contact" }
                end
              RUBY
            }

            Code(:html, title: "Rendered output"){
              <<~HTML
                <nav class="flex flex-row gap-4">
                  <a href="/" class="text-underline">Home</a>
                  <a href="/about" class="text-underline">About</a>
                  <a href="/contact" class="text-underline">Contact</a>
                </nav>
              HTML
            }
          }
        }
      },

      ContentSlide(
        title: "Set default & require values with method signatures",
        subtitle: "Ruby method signatures enforce required data and sets defaults"
      ){
        TwoUp {
          Code(:ruby, title: "Set default values in arguments"){
            <<~RUBY
              class TailwindNav < Phlex::HTML
                def initialize(title: "Main Menu")
                  @title = title
                end

                def view_template(&content)
                  h2(class: "font-bold") { @title }
                  nav(class: "flex flex-row gap-4", &content)
                end

                def item(url, &content)
                  a(href: url, class: "text-underline", &content)
                end
              end
            RUBY
          }

          Code(:ruby, title: "Override default method value"){
            <<~RUBY
              render TailwindNav.new title: "Site Menu" do |it|
                it.item("/") { "Home" }
                it.item("/about") { "About" }
                it.item("/contact") { "Contact" }
              end
            RUBY
          }
        }
      },

      ContentSlide(
        title: "Write beautiful code with Phlex Kits",
        subtitle: "Class functions automatically initialize and render Phlex components"
      ){
        Code(:ruby) {
          <<~RUBY
            class Page < ApplicationComponent
              include Phlex::Kit

              def view_template
                Sidebar {
                  Header { "My Site" }
                  p(class: "text-lg font-bold") { "Let's mix components with some HTML tags." }
                  TailwindNav title: "Site Menu" do |it|
                    it.item("/") { "Home" }
                    it.item("/about") { "About" }
                    it.item("/contact") { "Contact" }
                  end
                }
              end
            end
          RUBY
        }
      },

      ContentSlide(
        title: "Phlex is just Ruby!"
      ){
        Markdown {
          <<~MARKDOWN
          * Use `include` and `extend` to mix behaviors into views.
          * Compose views by rendering Phlex views within views.
          * Enforce data types with Ruby's type checking.
          * Distribute UI libraries via RubyGems.
          * More boring and less "stuff" than Erb and ViewComponents.
          MARKDOWN
        }
      },

      TitleSlide(
        title: "Use Phlex with Rails",
        subtitle: "Incrementally go from zero to hero",
        class: "bg-gradient-to-tl from-red-600 to-orange-600 text-white"
      ),

      ContentSlide(
        title: "Install Phlex Rails integration"
      ){
        Prose { "Install the phlex-rails gem:"}
        Code(:sh) {
          <<~SH
            $ gem install phlex-rails
            $ rails g phlex:install
          SH
        }
        Markdown {
          <<~MARKDOWN
          This changes a few things in your Rails project:

          * Adds view paths to `./config/application.rb`.
          * Creates view files in `./app/views` and `./app/views/components`.

          Reboot server to pick-up these changes!

          Make sure you checkout the website, [Phlex.fun](https://phlex.fun) for more examples and docs.
          MARKDOWN
        }
      },

      ContentSlide(
        title: "Render Phlex components from existing templates",
        subtitle: "Phlex components can be rendered from existing Erb, Slim, Haml, or Liquid views."
      ){
        Code(:erb, title: "Erb") {
          <<~HTML
            <h1>Hello</h1>
            <%= render TailwindNav.new title: "Site Menu" do |it| %>
              <% it.item("/") { "Home" } %>
              <% it.item("/about") { "About" } %>
              <% it.item("/contact") { "Contact" } %>
            <% end %>
          HTML
        }
        Code(:slim, title: "Slim") {
          <<~SLIM
            h1 Hello
            = render TailwindNav.new title: "Site Menu" do |it|
              - it.item("/") { "Home" }
              - it.item("/about") { "About" }
              - it.item("/contact") { "Contact" }
          SLIM
        }
      },

      ContentSlide(
        title: "Build pages with Phlex",
        subtitle: "Here's what a page might look like in Phlex"
      ){
        Code(:ruby) {
          <<~RUBY
            # ./app/views/profile.rb
            class Views::Profile < PageView
              def initialize(user:)
                @user = user
              end

              def view_template
                div class: "grid grid-cols-2 gap-8" do
                  render TailwindNav.new do |it|
                    it.item("/password") { "Change password" }
                    it.item("/logout") { "Log out" }
                    it.item("/settings") { "Settings" }
                  end

                  main do
                    h1 { "Hi #\{@user.name} "}
                  end
                end
              end
            end
          RUBY
        }
      },

      ContentSlide(
        title: "Page Layouts are superclasses",
        subtitle: "Pages inherit from a superclass that implements an `around_template`, wrapping the contents of `template` in the subclass."
      ){
        Code(:ruby) {
          <<~RUBY
            # ./app/views/page_view.rb
            class PageView < ApplicationComponent
              def around_template(&content)
                html do
                  head do
                    title { @title || "My Site" }
                  end
                  body(&content)
                end
              end
            end
          RUBY
        }
      },

      ContentSlide(
        title: "Render Phlex pages from controllers",
        subtitle: "Render an instance of a Phlex view from a controller action."
      ){
        Code(:ruby) {
          <<~RUBY
            class ProfileController < ApplicationController
              before_action { @user = User.find(params.fetch(:id)) }

              def show
                respond_to do |format|
                  format.html { render Views::Profile.new(user: @user) }
                end
              end
            end
          RUBY
        }
      },

      TitleSlide(
        title: "The ambitious possibilities of Phlex",
        subtitle: "A few projects that get me excited about the future of Phlex",
        class: "bg-gradient-to-tl from-green-500 to-blue-500 text-white"
      ),

      TitleSlide(
        title: "Superview",
        subtitle: "Build Rails applications, from the ground up, using Phlex components",
        class: "bg-gradient-to-tl from-slate-500 to-slate-800 text-white"
      ),

      ContentSlide(
        title: "Inline Views",
        subtitle: "Start building out views in the controller, kinda like building apps in Sinatra"
      ){
        Code(:ruby) {
          <<~RUBY
            class BlogsController < ApplicationController
              class Index < ApplicationView
                attr_writer :blogs

                def view_template
                  h1 { "Blogs" }
                  ul {
                    @blogs.each do |blog|
                      li { a(href: blog_path(blog)) { blog.title } }
                    end
                  }
                end
              end

              def index
                @blogs = Blog.all
                render component(Index)
              end
            end
          RUBY
        }
      },

      ContentSlide(
        title: "Extracted Views",
        subtitle: "Move views to `./app/views/*` folder to organize or share with other controllers."
      ){
        TwoUp {
          Code(:ruby, title: "Controller") {
            <<~RUBY
              class PostsController < ApplicationController
                def index
                  @posts = Post.all
                  render component(Posts::Index)
                end
              end
            RUBY
          }
          Code(:ruby, title: "View") {
            <<~RUBY
              class Posts::Index < ApplicationView
                attr_writer :posts

                def view_template
                  h1 { "Posts" }
                  ul {
                    @posts.each do |post|
                      li { post.title }
                    end
                  }
                end
              end
            RUBY
          }
        }
      },

      TitleSlide(
        title: "Superform",
        subtitle: "The best way to build forms in Rails applications",
        class: "bg-gradient-to-r from-teal-400 to-yellow-200 text-black"
      ),

      ContentSlide(
        title: "This is a simple blog post Superform"
      ){
        Code(:ruby) {
          <<~RUBY
            class Posts::Form < ApplicationForm
              def view_template
                render field(:title).input.focus
                render field(:body).textarea(rows: 6)
                submit "Save"
              end
            end
          RUBY
        }
      },

      ContentSlide(
        title: "Here's a complex sign-up Superform"
      ){
        Code(:ruby) {
          <<~RUBY
            # Everything below is intentionally verbose!
            class SignupForm < ApplicationForm
              def view_template
                render field(:name).input.focus
                render field(:email).input(type: :email, placeholder: "We will sell this to third parties", required: true)

                render field(:reason) do |f|
                  div do
                    f.label { "Why should we care about you?" }
                    f.textarea(row: 3, col: 80)
                  end
                end

                div do
                  render field(:contact).label { "Would you like us to spam you to death?" }
                  render field(:contact).select(
                    [true, "Yes"],
                    [false, "No"],
                    "Hell no",
                    nil
                  )
                end

                render button { "Submit" }
              end
            end
          RUBY
        }
      },

      ContentSlide(
        title: "Superform can permit its own parameters",
        subtitle: "It's been almost two years since the last time a forgotten strong parameter caused a bug"
      ){
        Code(:ruby) {
          <<~RUBY
            class ProfileController < ApplicationController
              class Form < ApplicationForm
                render field(:name).input
                render field(:email).input(type: :email)
                button { "Save" }
              end

              before_action do
                @user = User.find(params.fetch(:id))
                @form = Form.new(@user)
              end

              def update
                @form.assign params.require(:user)
                @user.save ? redirect_to(@user) : render(@form)
              end
            end
          RUBY
        }
      },

      TitleSlide(
        title: "Rails Apps using Phlex Components",
        subtitle: "A few projects built and shipped to production with Phlex"
      ),

      ContentSlide(
        title: "TinyZap",
        subtitle: "My first 100% Phlex production app. Used for UI and OpenGraph image generation."
      ){
        a(href: "https://tinyzap.com/"){
          img(src: "https://objects.bradgessler.com/Screenshot-2024-06-13-at-12.41.05.png")
        }
      },

      ContentSlide(
        title: "Legible News",
        subtitle: "Migrating templates from Slim & Erb to Phlex as I enhance the app."
      ){
        a(href: "https://legiblenews.com/"){
          img(src: "https://objects.bradgessler.com/Shared-Image-2024-06-13-12-50-54.png")
        }
      },

      TitleSlide(
        title: "Hosted on Fly.io",
        class: "bg-violet-950 text-white"
      ){
        a class: "flex flex-col gap-12 justify-center items-center", href: "https://fly.io/" do
          img(src: "https://objects.bradgessler.com/logo-portrait-light.svg", class: "h-96")
          Subtitle { "All of these apps are deployed and running on Fly.io" }
        end
      },

      ContentSlide(
        title: "Thingybase",
        subtitle: "Migrating templates from Slim to Phlex as I enhance the app."
      ){
        a(href: "https://www.thingybase.com/"){
          img(src: "https://objects.bradgessler.com/Shared-Image-2024-06-13-12-45-56.png")
        }
      },

      TitleSlide(
        title: "Open Source Rails Apps",
        subtitle: "Projects you can look at to see how to use Phlex in Rails"
      ),

      ContentSlide(title: "Ruby Monolith Blog Demo"){
        Markdown { "Source available at https://github.com/rubymonolith/demo" }
        a(href: "https://demo.rubymonolith.com/"){
          img(src: "https://objects.bradgessler.com/Shared-Image-2024-06-13-13-25-58.png")
        }
      },

      ContentSlide(title: "This presentation was built with Phlex"){
        Markdown { "Source available at [https://github.com/beautifulruby](https://github.com/beautifulruby)" }
        Code(:ruby,
          class: "overflow-scroll",
          file: __FILE__
        )
      },

      TitleSlide(
        title: "Phlex Dreams",
        subtitle: "A few projects in their early stages that I hope come to life"
      ),

      ContentSlide(
        title: "UI Toolkits built with Phlex"
      ){
        Markdown { "A few already exist like [PhlexUI](https://phlexui.com) and [ZestUI](https://zestui.com)." }
        a(href: "https://phlexui.com") {
          img(src: "https://objects.bradgessler.com/Shared-Image-2024-06-13-13-18-09.png")
        }
      },

      ContentSlide(
        title: "Site Phlex",
        subtitle: "Building content pages with Front Matter could look like this:"
      ){
        Code(:ruby){
          <<~RUBY
            # ./app/content/pages/index.phlex.html
            LandingPage(
              title: "Get it now"
            ){
              Hero {
                Title { @title }
                Subtitle { "It's the best thing you'll ever get." }
                button(class: "btn btn-primary") { "Sign up" }
              }
              section {
                h2 { "Features" }
                Markdown {
                  <<~MARKDOWN
                  Here's everything you get:

                  * A thing that goes "Ping!"
                  * A bunch of extra batteries
                  * A thing that goes "Boom!"
                  MARKDOWN
                }
              }
            }
          RUBY
        }
      },

      ContentSlide(
        title: "Ruby Monolith"
      ){
        a(href: "https://rubymonolith.com/"){
          img(src: "https://objects.bradgessler.com/Shared-Image-2024-06-13-13-30-26.png")
        }
      },

      ContentSlide(
        title: "That's a wrap! Any questions?",
      ){
        TwoUp {
          Markdown {
            <<~MARKDOWN
            Thanks for listening! Here are some useful links:

            * [Phlex.fun](https://phlex.fun)
            * [Ruby Monolith Demo](https://demo.rubymonolith.com)
            * [Fly.io](https://fly.io)
            * [Beautiful Ruby](https://beautifulruby.com)

            Find me on Bluesky [@bradgessler](https://bsky.app/profile/bradgessler.com) or Twitter [@bradgessler](https://twitter.com/bradgessler).

            MARKDOWN
          }
        }
      },

    ]
  end
end
34 / 39