Skip to content

Commit

Permalink
Add Viewable callable object mixin for Ruby components (#959)
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredcwhite authored Jan 3, 2025
1 parent 4d3eea6 commit 16128e0
Show file tree
Hide file tree
Showing 15 changed files with 209 additions and 4 deletions.
1 change: 1 addition & 0 deletions bridgetown-core/lib/bridgetown-core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ module Bridgetown
autoload :Slot, "bridgetown-core/slot"
autoload :StaticFile, "bridgetown-core/static_file"
autoload :Transformable, "bridgetown-core/concerns/transformable"
autoload :Viewable, "bridgetown-core/concerns/viewable"
autoload :Utils, "bridgetown-core/utils"
autoload :VERSION, "bridgetown-core/version"
autoload :Watcher, "bridgetown-core/watcher"
Expand Down
46 changes: 46 additions & 0 deletions bridgetown-core/lib/bridgetown-core/concerns/viewable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

module Bridgetown
# This mixin for Bridgetown components allows you to provide front matter and render
# the component template via the layouts transformation pipeline, which can be called
# from any Roda route
module Viewable
include Bridgetown::RodaCallable
include Bridgetown::Transformable

def site
@site ||= Bridgetown::Current.site
end

def data
@data ||= HashWithDotAccess::Hash.new
end

def front_matter(&block)
Bridgetown::FrontMatter::RubyFrontMatter.new(data:).tap { _1.instance_exec(&block) }
end

def relative_path = self.class.source_location.delete_prefix("#{site.root_dir}/")

# Render the component template in the layout specified in your front matter
#
# @param app [Roda]
def render_in_layout(app)
render_in(app) => rendered_output

site.validated_layouts_for(self, data.layout).each do |layout|
transform_with_layout(layout, rendered_output, self) => rendered_output
end

rendered_output
end

# Pass a block of front matter and render the component template in layouts
#
# @param app [Roda]
def render_with(app, &)
front_matter(&)
render_in_layout(app)
end
end
end
4 changes: 2 additions & 2 deletions bridgetown-core/lib/bridgetown-core/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class Helpers
include ::Streamlined::Helpers
include Inclusive

# @return [Bridgetown::RubyTemplateView]
# @return [Bridgetown::RubyTemplateView, Bridgetown::Component]
attr_reader :view

# @return [Bridgetown::Site]
Expand All @@ -22,7 +22,7 @@ class Helpers
# @return [Bridgetown::Foundation::SafeTranslations]
packages def translate_package = [Bridgetown::Foundation::Packages::SafeTranslations]

# @param view [Bridgetown::RubyTemplateView]
# @param view [Bridgetown::RubyTemplateView, Bridgetown::Component]
# @param site [Bridgetown::Site]
def initialize(view = nil, site = nil)
@view = view
Expand Down
2 changes: 1 addition & 1 deletion bridgetown-core/lib/roda/plugins/bridgetown_ssr.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def self.load_dependencies(app)

# This lets us return callable objects directly in Roda response blocks
app.handle_block_result(Bridgetown::RodaCallable) do |callable|
callable.(self)
request.send :block_result_body, callable.(self)
end
end

Expand Down
3 changes: 2 additions & 1 deletion bridgetown-core/test/ssr/config.ru
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require "bridgetown-core/rack/boot"

Bridgetown::Rack.boot

require_relative "src/_components/UseRoda" # normally Zeitwerk would take care of this for us
require_relative "src/_components/page_me" # normally Zeitwerk would take care of this for us
require_relative "src/_components/use_roda" # normally Zeitwerk would take care of this for us

run RodaApp.freeze.app # see server/roda_app.rb
4 changes: 4 additions & 0 deletions bridgetown-core/test/ssr/server/routes/render_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,9 @@ class Routes::RenderResource < Bridgetown::Rack::Routes
r.get "render_component", String do |title|
UseRoda.new(title:)
end

r.get "render_view", String do |title|
PageMe.new(title:)
end
end
end
9 changes: 9 additions & 0 deletions bridgetown-core/test/ssr/src/_components/page_me.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<h2><%= @title %></h2>

<%= markdownify do %>
* Port <%= @port_number %>

<%= render Shared::Stuff.new(wild: 123) do %>
_ya think?_
<% end %>
<% end %>
20 changes: 20 additions & 0 deletions bridgetown-core/test/ssr/src/_components/page_me.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

class PageMe < Bridgetown::Component
include Bridgetown::Viewable

def initialize(title:) # rubocop:disable Lint/MissingSuper
@title = title.upcase

data.title = @title
end

def call(app)
@port_number = app.request.port

render_with(app) do
layout :page
page_class "some-extras"
end
end
end
11 changes: 11 additions & 0 deletions bridgetown-core/test/ssr/src/_components/shared/stuff.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

class Shared::Stuff < Bridgetown::Component
def initialize(wild:) # rubocop:disable Lint/MissingSuper
@wild = wild * 2
end

def template
"Well that was #{@wild}!#{content}"
end
end
1 change: 1 addition & 0 deletions bridgetown-core/test/ssr/src/_data/site_metadata.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
title: So Awesome
5 changes: 5 additions & 0 deletions bridgetown-core/test/ssr/src/_layouts/default.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<title><%= data.title %> | <%= site.metadata.title %></title>

<body class="<%= data.layout %> <%= data.page_class %>">
<%= yield %>
</body>
7 changes: 7 additions & 0 deletions bridgetown-core/test/ssr/src/_layouts/page.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
layout: default
---

<h1><%= data.title %></h1>

<%= yield %>
11 changes: 11 additions & 0 deletions bridgetown-core/test/test_ssr.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,16 @@ def site
assert_equal "application/rss+xml", last_response["Content-Type"]
assert_equal "<rss>WOW true</rss>", last_response.body
end

should "return rendered view" do
get "/render_view/Page_Me"

assert last_response.ok?
assert_includes last_response.body, "<title>PAGE_ME | So Awesome</title>"
assert_includes last_response.body, "<body class=\"page some-extras\">"
assert_includes last_response.body, "<h1>PAGE_ME</h1>"
assert_includes last_response.body, "<ul>\n <li>Port 80</li>\n</ul>"
assert_includes last_response.body, "<p>Well that was 246!\n <em>ya think?</em></p>"
end
end
end
89 changes: 89 additions & 0 deletions bridgetown-website/src/_docs/routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,50 @@ You can return a resource at the end of any Roda block to have it render out aut
Most of the time though, on modestly-sized sites, this shouldn't prove to be a major issue.
{% end %}

## Rendering Viewable Components

For a traditional "VC" part of the MVC (Model-View-Controller) programming paradigm, Bridgetown provides a `Viewable` mixin for [components](/_docs/components). This lets you offload the rendering of a view to a component, keeping your Roda route very clean.

```ruby
# ./server/routes/products.rb

class Routes::Products < Bridgetown::Rack::Routes
route do |r|
r.on "products" do
# route: /products/:sku
r.get String do |sku|
# Tip: check out bridgetown_sequel plugin for database connectivity!
Views::Product.new product: Product.find(sku:)
end
end
end
end

# ./src/_components/views/product.rb

class Views::Product < Bridgetown::Component
include Bridgetown::Viewable

def initialize(product:) # rubocop:disable Lint/MissingSuper
@product = product

data.title = @product.title
end

# @param app [Roda] this is the instance of the Roda application
def call(app)
render_with(app) do
layout :page
page_class "product"
end
end
end

# ./src/_components/views/product.erb is an exercise left to the reader
```
[Read more about the callable objects pattern below.](#callable-objects-for-rendering-within-blocks)
## File-based Dynamic Routes
**But wait, there’s more!** We also provide a plugin called `bridgetown-routes` which gives you the ability to write file-based dynamic routes with integrated view templates right inside your source folder.
Expand Down Expand Up @@ -293,6 +337,51 @@ end

For the Roda-curious, we've enabled this behavior via our own custom handler for the `custom_block_results` Roda plugin.

And [as mentioned previously](#rendering-viewable-components), the `Viewable` component mixin is a wrapper around `RodaCallable` to add some extra smarts to [Ruby components](/docs/components):

* You can access the `data` hash from within your component to add and retrieve front matter for the view.
* You can call `front_matter` with a block to define [Ruby Front Matter](/docs/front-matter#the-power-of-ruby-in-front-matter) for the view.
* From your `call(app)` method, you can call `render_in_layout(app)` to render the component template within the layout defined via your front matter.
* Or for a shorthand, call `render_with(app) do ... end` to specify Ruby Front Matter and render the template in one pass.

You can even cascade multiple callable objects, if you really want a full object-oriented MVC experience:

```ruby
# ./server/routes/products.rb

class Routes::Reports < Bridgetown::Rack::Routes
route do |r|
r.on "reports" do
# route: /reports/:id
r.get Integer do |id|
Controllers::Reports::Show.new(id:)
end
end
end
end

# ./server/controllers/reports/show.rb

class Controllers::Reports::Show
include Bridgetown::RodaCallable

def initialize(id:)
@id = id
end

def call(app)
app => { request:, response: }

report = Report[@id]

# do other bits of controller-y logic here

# render a Viewable component
Views::Reports::Show.new(report:)
end
end
```

<!--
## Roda Helpers
Expand Down

0 comments on commit 16128e0

Please sign in to comment.