From a578dcfd852d11e44e3ee8c21e9260fa49615475 Mon Sep 17 00:00:00 2001 From: Joel Drapper Date: Tue, 11 Oct 2022 18:38:40 +0100 Subject: [PATCH] Update docs --- Gemfile | 3 +- Procfile.dev | 3 + bin/docs | 19 +-- docs/build.rb | 25 ++-- docs/components/code_span.rb | 9 ++ docs/components/example.rb | 2 +- docs/components/heading.rb | 2 +- docs/components/layout.rb | 52 +++++--- docs/components/markdown.rb | 20 ++- docs/components/nav.rb | 6 + docs/components/nav/item.rb | 33 +++++ docs/components/title.rb | 2 +- docs/page_builder.rb | 3 + docs/pages/helpers.rb | 97 +++++++++++++++ docs/pages/index.rb | 23 +--- docs/pages/library/collections.rb | 101 +++++++++++++++ docs/pages/rails/getting_started.rb | 53 ++++++++ docs/pages/rails/helpers.rb | 53 ++++++++ docs/pages/rails/layouts.rb | 61 +++++++++ docs/pages/rails/migrating.rb | 37 ++++++ docs/pages/rails/rendering_views.rb | 35 ++++++ docs/pages/templates.rb | 187 +++++++--------------------- docs/pages/views.rb | 149 ++++++++-------------- lib/phlex/rails/helpers.rb | 6 + lib/phlex/view.rb | 12 +- 25 files changed, 685 insertions(+), 308 deletions(-) create mode 100644 Procfile.dev create mode 100644 docs/components/code_span.rb create mode 100644 docs/components/nav.rb create mode 100644 docs/components/nav/item.rb create mode 100644 docs/pages/helpers.rb create mode 100644 docs/pages/library/collections.rb create mode 100644 docs/pages/rails/getting_started.rb create mode 100644 docs/pages/rails/helpers.rb create mode 100644 docs/pages/rails/layouts.rb create mode 100644 docs/pages/rails/migrating.rb create mode 100644 docs/pages/rails/rendering_views.rb diff --git a/Gemfile b/Gemfile index f099eaed..cfe3f3f5 100644 --- a/Gemfile +++ b/Gemfile @@ -10,7 +10,6 @@ gem "rake" gem "sus", group: [:test] gem "rails", group: [:test] gem "rouge", group: [:docs] -gem "listen", group: [:docs] gem "webrick", group: [:docs] gem "zeitwerk", group: [:docs] gem "redcarpet", group: [:docs] @@ -20,3 +19,5 @@ gem "htmlbeautifier", group: [:docs] gem "benchmark-memory" gem "rubocop", require: false, github: "joeldrapper/rubocop", branch: "rubocop-user-agent" gem "syntax_suggest" +gem "foreman" +gem "filewatcher", group: [:docs] diff --git a/Procfile.dev b/Procfile.dev new file mode 100644 index 00000000..0478b4fa --- /dev/null +++ b/Procfile.dev @@ -0,0 +1,3 @@ +server: ruby -run -e httpd docs/dist +docs: bundle exec docs/build.rb --watch +tailwind: npx tailwindcss -i ./docs/assets/application.css -o ./docs/dist/application.css --watch diff --git a/bin/docs b/bin/docs index 01faba1d..e1ce821c 100755 --- a/bin/docs +++ b/bin/docs @@ -1,18 +1 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require "listen" - -system "bundle exec docs/build.rb" - -Listen.to("#{__dir__}/../docs/pages", "#{__dir__}/../docs/components") do |modified, added, removed| - puts modified - puts added - puts removed - - system "bundle exec docs/build.rb" -end.start - -system "ruby -run -e httpd docs/dist" - -Process.waitall +foreman start -f Procfile.dev diff --git a/docs/build.rb b/docs/build.rb index 77660949..fbc7a14b 100755 --- a/docs/build.rb +++ b/docs/build.rb @@ -1,22 +1,27 @@ #!/usr/bin/env ruby # frozen_string_literal: true +$stdout.sync = true + require "phlex" require "bundler" require "fileutils" Bundler.require :docs -Zeitwerk::Loader.new.tap do |loader| - loader.push_dir(__dir__) - loader.ignore(__FILE__) - loader.setup - loader.eager_load -end - -FileUtils.mkdir_p("#{__dir__}/dist") -FileUtils.cp_r("#{__dir__}/assets", "#{__dir__}/dist") +loader = Zeitwerk::Loader.new +loader.push_dir(__dir__) +loader.ignore(__FILE__) +loader.enable_reloading +loader.setup +loader.eager_load PageBuilder.build_all -system "npx tailwindcss -i ./docs/assets/application.css -o ./docs/dist/application.css" +if ARGV.include? "--watch" + Filewatcher.new("#{__dir__}/**/*rb").watch do |_changes| + loader.reload + loader.eager_load + PageBuilder.build_all + end +end diff --git a/docs/components/code_span.rb b/docs/components/code_span.rb new file mode 100644 index 00000000..70ea9f99 --- /dev/null +++ b/docs/components/code_span.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Components + class CodeSpan < Phlex::View + def template(&block) + code(class: "bg-stone-50 inline-block font-medium rounded border px-1 -mt-1", &block) + end + end +end diff --git a/docs/components/example.rb b/docs/components/example.rb index 3f2f881b..1d956289 100644 --- a/docs/components/example.rb +++ b/docs/components/example.rb @@ -24,7 +24,7 @@ def tab(name, code) def execute(code) output = @sandbox.class_eval(code) - @t.tab("HTML Output") do + @t.tab("๐Ÿ‘€ Output") do render CodeBlock.new(HtmlBeautifier.beautify(output), syntax: :html) end end diff --git a/docs/components/heading.rb b/docs/components/heading.rb index d3d89f9d..b8fd71a8 100644 --- a/docs/components/heading.rb +++ b/docs/components/heading.rb @@ -3,7 +3,7 @@ module Components class Heading < Phlex::View def template(&block) - h2(class: "text-xl font-bold mt-10 mb-5", &block) + h2(class: "text-2xl font-semibold mt-10 mb-5") { raw(&block) } end end end diff --git a/docs/components/layout.rb b/docs/components/layout.rb index 4bef64f8..392a56bf 100644 --- a/docs/components/layout.rb +++ b/docs/components/layout.rb @@ -19,25 +19,47 @@ def template(&block) style { raw Rouge::Theme.find("github").render(scope: ".highlight") } end - body class: "p-12" do - div class: "max-w-screen-lg mx-auto grid grid-cols-4 gap-10" do - header class: "col-span-1" do - a(href: "/", class: "block") { img src: "/assets/logo.png", width: "150" } - - nav do - ul do - li { a(href: "/") { "Introduction" } } - li { a(href: "/templates") { "Templates" } } - li { a(href: "/views") { "Views" } } - li { a(href: "/rails-integration") { "Rails integration" } } - li { a(href: "https://github.com/joeldrapper/phlex") { "Source code" } } - end + body class: "text-stone-700" do + header class: "border-b py-4 px-10 flex justify-between items-center" do + a(href: "/", class: "block") { img src: "/assets/logo.png", width: "100" } + + nav(class: "text-stone-500 font-medium") do + ul(class: "flex space-x-8") do + li { a(href: "https://github.com/sponsors/joeldrapper") { "๐Ÿ’–๏ธ Sponsor" } } + li { a(href: "https://github.com/joeldrapper/phlex") { "GitHub" } } end end + end + + div class: "grid grid-cols-4 divide-x" do + nav class: "col-span-1 px-10 py-5" do + h2(class: "text-lg font-semibold pt-5") { "Guide" } + + ul do + render Nav::Item.new("Introduction", to: Pages::Index, active_page: @_parent) + render Nav::Item.new("Views", to: Pages::Views, active_page: @_parent) + render Nav::Item.new("Templates", to: Pages::Templates, active_page: @_parent) + render Nav::Item.new("Helpers", to: Pages::Helpers, active_page: @_parent) + end + + h2(class: "text-lg font-semibold pt-5") { "Rails" } - main(class: "col-span-3", &block) + ul do + render Nav::Item.new("Getting started", to: Pages::Rails::GettingStarted, active_page: @_parent) + render Nav::Item.new("Rendering views", to: Pages::Rails::RenderingViews, active_page: @_parent) + render Nav::Item.new("Laouts", to: Pages::Rails::Layouts, active_page: @_parent) + render Nav::Item.new("Helpers", to: Pages::Rails::Helpers, active_page: @_parent) + render Nav::Item.new("Migrating to Phlex", to: Pages::Rails::Migrating, active_page: @_parent) + end + end + + main class: "col-span-3 px-20 px-10 py-5" do + div(class: "max-w-prose prose", &block) + end + end - footer class: "text-sm text-right col-span-4 py-10" + footer class: "border-t p-20 flex justify-center text-stone-500 text-lg font-medium" do + a(href: "https://github.com/sponsors/joeldrapper") { "Sponsor this project ๐Ÿ’–" } end end end diff --git a/docs/components/markdown.rb b/docs/components/markdown.rb index b7d2ca65..d55fa7e4 100644 --- a/docs/components/markdown.rb +++ b/docs/components/markdown.rb @@ -3,17 +3,31 @@ module Components class Markdown < Phlex::View class Render < Redcarpet::Render::HTML + include Redcarpet::Render::SmartyPants + def header(text, level) case level when 1 - Title.new.call { text } + Title.new.call { CGI.unescapeHTML(text) } else - Heading.new.call { text } + Heading.new.call { CGI.unescapeHTML(text) } end end + + def codespan(code) + CodeSpan.new.call { code } + end + + def block_code(code, language) + CodeBlock.new(code.gsub(/(?:^|\G) {4}/m, " "), syntax: language).call + end + + def html_escape(input) + input + end end - MARKDOWN = Redcarpet::Markdown.new(Render.new, autolink: true, tables: true) + MARKDOWN = Redcarpet::Markdown.new(Render.new, filter_html: false, autolink: true, fenced_code_blocks: true, tables: true, highlight: true, escape_html: false) def initialize(content) @content = content diff --git a/docs/components/nav.rb b/docs/components/nav.rb new file mode 100644 index 00000000..8ef059e3 --- /dev/null +++ b/docs/components/nav.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Components + class Nav < Phlex::View + end +end diff --git a/docs/components/nav/item.rb b/docs/components/nav/item.rb new file mode 100644 index 00000000..5edd86b1 --- /dev/null +++ b/docs/components/nav/item.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Components + class Nav::Item < Phlex::View + def initialize(text, to:, active_page:) + @text = text + @to = to + @active_page = active_page + end + + def template + li do + a(**link_classes, href: "/#{link}") { @text } + end + end + + def link_classes + classes("pb-1 block font-medium text-stone-500", active?: "text-red-600 font-bold") + end + + def link + path == "index" ? "" : path + end + + def path + @to.name.split("::")[1..].map { _1.gsub(/(.)([A-Z])/, '\1-\2') }.map(&:downcase).join("/") + end + + def active? + @active_page.instance_of?(@to) + end + end +end diff --git a/docs/components/title.rb b/docs/components/title.rb index b76ebef6..f863c804 100644 --- a/docs/components/title.rb +++ b/docs/components/title.rb @@ -3,7 +3,7 @@ module Components class Title < Phlex::View def template(&block) - h1(class: "text-2xl font-bold my-5", &block) + h1(class: "text-3xl font-semibold my-5") { raw(&block) } end end end diff --git a/docs/page_builder.rb b/docs/page_builder.rb index d9cd4270..4551b7f2 100644 --- a/docs/page_builder.rb +++ b/docs/page_builder.rb @@ -4,6 +4,8 @@ class PageBuilder ROOT = Pages::ApplicationPage def self.build_all + FileUtils.mkdir_p("#{__dir__}/dist") + FileUtils.cp_r("#{__dir__}/assets", "#{__dir__}/dist") ROOT.subclasses.each { |page| new(page).call } end @@ -12,6 +14,7 @@ def initialize(page) end def call + puts "Building #{@page.name}" FileUtils.mkdir_p(directory) File.write(file, @page.new.call) end diff --git a/docs/pages/helpers.rb b/docs/pages/helpers.rb new file mode 100644 index 00000000..c74ebd71 --- /dev/null +++ b/docs/pages/helpers.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +module Pages + class Helpers < ApplicationPage + def template + render Layout.new(title: "Templates in Phlex") do + render Markdown.new(<<~MD) + # Helpers + + ## Conditional tokens and classes + + The `tokens` method helps you define conditional HTML attribute tokens (such as CSS classes). It accepts a splat of tokens that should always be output as well as optional keyword arguments for conditional tokens. + + The keyword arguments allow you to specify under which conditions certain tokens are applicable. The keys are the conditions and the values are the tokens. Conditions can be Procs which are evaluated, or Symbols that map to an instance method. The `:active?` Symbol, for example, maps to the `active?` instance method. + + Here we have a `Link` view that produces an `` tag with the CSS class `nav-item`. If the link is _active_, we also apply the CSS class `nav-item-active`. + MD + + render Example.new do |e| + e.tab "link.rb", <<~RUBY + class Link < Phlex::View + def initialize(text, to:, active:) + @text = text + @to = to + @active = active + end + + def template + a(href: @to, class: tokens("nav-item", + active?: "nav-item-active")) { @text } + end + + private + + def active? = @active + end + RUBY + + e.tab "example.rb", <<~RUBY + class Example < Phlex::View + def template + nav do + ul do + li { render Link.new("Home", to: "/", active: true) } + li { render Link.new("About", to: "/about", active: false) } + end + end + end + end + RUBY + + e.execute "Example.new.call" + end + + render Markdown.new(<<~MD) + You can also use the `classes` helper method to create a token list of classes. Since this method returns a hash, e.g. `{ class: "your CSS classes here" }`, you can destructure it into a `class:` keyword argument using the `**` prefix operator. + MD + + render Example.new do |e| + e.tab "link.rb", <<~RUBY + class Link < Phlex::View + def initialize(text, to:, active:) + @text = text + @to = to + @active = active + end + + def template + a(href: @to, **classes("nav-item", + active?: "nav-item-active")) { @text } + end + + private + + def active? = @active + end + RUBY + + e.tab "example.rb", <<~RUBY + class Example < Phlex::View + def template + nav do + ul do + li { render Link.new("Home", to: "/", active: true) } + li { render Link.new("About", to: "/about", active: false) } + end + end + end + end + RUBY + + e.execute "Example.new.call" + end + end + end + end +end diff --git a/docs/pages/index.rb b/docs/pages/index.rb index 1b00a287..90e43372 100644 --- a/docs/pages/index.rb +++ b/docs/pages/index.rb @@ -3,34 +3,23 @@ module Pages class Index < ApplicationPage def template - render Layout.new(title: "Introduction to Phlex") do + render Layout.new(title: "Introduction to Phlex, a fast, object-oriented view framework for Ruby") do render Markdown.new(<<~MD) # Introduction Phlex is a framework for building fast, reusable, testable views in pure Ruby. - Each view object is an instance of a specific class of view. The nav-bar, for example, might contain three different nav-bar-items, but theyโ€™re all instances of the nav-bar-item class. This class, then, manifests everything there is to know about nav bar items in general. It models: + ## Better developer experience ๐Ÿ’ƒ - 1. the **data** attributes being represented โ€” perhaps url and label; - 2. a **template**, which dictates how the data should be represented with HTML markup and CSS classes; and - 3. **logic**, conditions, or calculations on the data โ€” perhaps a predicate method to determine whether the link is active or not. - - # Why use Phlex? - - ## Better developer experience ๐ŸŽ‰ - - You donโ€™t need to introduce a new language like Slim, HAML, or ERB. Phlex views are plain old Ruby objects: view classes are just Ruby classes, templates are just methods, and HTML tags are just method calls. If you know how to define a method that calls another method, you pretty much already know how to use Phlex. + Phlex views are plain old Ruby objects. View classes are just Ruby classes, templates are just methods, and HTML tags are just method calls. If you know how to define a method that calls another method, you pretty much already know how to use Phlex. ## Better safety ๐Ÿฅฝ - Rails partials can implicitly depend on instance variables from a controller or view. This happens more often than you might think when copying code into a new partial extraction. If the partial is then rendered in a different context or the instance variableโ€™s meaning changes, things can break quite severely without warning. - - Conversely, Phlex view templates render in an isolated execution context where only the instance variables and methods for the specific view are exposed. - ## Better performance ๐Ÿš€ + Phlex view templates render in an isolated execution context where only the instance variables and methods for the specific view are exposed. - Phlex is ~4.35ร— faster than ActionView and ~2ร— faster than ViewComponent. Phlex views are also streamable. + ## Better performance ๐Ÿ”ฅ - Rails apps typically spend 40-80% of the response time rendering views, so this could be a significant factor in overall app performance. + Rendering a Phlex view is ~4.35ร— faster than an ActionView partial and ~2ร— faster than ViewComponent component. MD end end diff --git a/docs/pages/library/collections.rb b/docs/pages/library/collections.rb new file mode 100644 index 00000000..ce6e28fb --- /dev/null +++ b/docs/pages/library/collections.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +module Pages + module Library + class Collections < ApplicationPage + def template + render Layout.new(title: "Getting started with Rails") do + render Markdown.new <<~MD + # Collections + + Phlex comes with an abstract pattern for views that represent collections of resources โ€” lists, grids, tables, etc. Collections have two parts: one part wraps the whole collection, the other part is repeated once for each item in that collection. + + When you include `Phlex::Collection` in a `Phlex::View`, the `template` and `initialize` methods are defined for you. You don't need to define these. Instead, you define a `collection_template` and `item_template`. + + ## Collection template + + The `collection_template` method should accept a content block which is used to yield the items. We can yield this block or pass it to another element, such as `