Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Viewable callable object mixin for Ruby components #959

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bridgetown-core/lib/bridgetown-core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,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
Loading