Skip to content

Commit

Permalink
Merge pull request #4 from benpickles/layout
Browse files Browse the repository at this point in the history
Add support for wrapping a Phlex view in a layout
  • Loading branch information
benpickles authored Sep 10, 2024
2 parents c66c146 + 9c71cb7 commit c81f764
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 59 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ source 'https://rubygems.org'
gemspec

gem 'capybara'
gem 'haml'
gem 'puma'
gem 'rack-test'
gem 'rake'
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,28 @@ get '/foo' do
end
```

## Layout

If your entire view layer uses Phlex then layout will be a part of your component structure but maybe you've got an existing non-Phlex layout or you don't want to use Phlex for _everything_, in which case standard Sinatra layouts are supported.

Pass `layout: true` to wrap the Phlex output with Sinatra's default layout -- a file named "layout.erb" in the configured views directory (ERB is the default) -- or pass a symbol to specify the file:

```ruby
get '/foo' do
# This Phlex view will be wrapped by `views/my_layout.erb`.
phlex MyView.new, layout: :my_layout
end
```

Other [Sinatra templating languages](https://sinatrarb.com/intro.html#available-template-languages) can be specified via the `layout_engine` keyword:

```ruby
get '/foo' do
# This Phlex view will be wrapped by `views/layout.haml`.
phlex MyView.new, layout: true, layout_engine: :haml
end
```

## Streaming

Streaming a Phlex view can be enabled by passing `stream: true` which will cause Phlex to automatically write to the response after the closing `</head>` and buffer the remaining content:
Expand Down
27 changes: 24 additions & 3 deletions lib/phlex-sinatra.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
module Phlex
module Sinatra
Error = Class.new(StandardError)
IncompatibleOptionError = Class.new(Error)

class TypeError < Error
MAX_SIZE = 32
Expand Down Expand Up @@ -37,18 +38,38 @@ class SGML

module Sinatra
module Templates
def phlex(obj, content_type: nil, stream: false)
def phlex(
obj,
content_type: nil,
layout: false,
layout_engine: :erb,
stream: false
)
raise Phlex::Sinatra::TypeError.new(obj) unless obj.is_a?(Phlex::SGML)

content_type ||= :svg if obj.is_a?(Phlex::SVG)
content_type ||= :svg if obj.is_a?(Phlex::SVG) && !layout
self.content_type(content_type) if content_type

# Copy Sinatra's behaviour and interpret layout=true as meaning "use the
# default layout" - uses an internal Sinatra instance variable :s
layout = @default_layout if layout == true

if stream
raise Phlex::Sinatra::IncompatibleOptionError.new(
'streaming is not compatible with layout'
) if layout

self.stream do |out|
obj.call(out, view_context: self)
end
else
obj.call(view_context: self)
output = obj.call(view_context: self)

if layout
render(layout_engine, layout, { layout: false }) { output }
else
output
end
end
end
end
Expand Down
106 changes: 51 additions & 55 deletions spec/phlex/sinatra_spec.rb → spec/general_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,6 @@ def view_template
end
end

class StreamingView < Phlex::HTML
def view_template
html {
head {
title { 'Streaming' }
}
body {
p { 1 }
flush # Internal private Phlex method.
p { 2 }
}
}
end
end

class SvgElem < Phlex::SVG
def view_template
svg { rect(width: 100, height: 100) }
Expand Down Expand Up @@ -75,14 +60,27 @@ class TestApp < Sinatra::Application
phlex MoreDetailsView.new
end

get '/stream' do
phlex StreamingView.new, stream: true
get '/more-with-layout' do
layout = params[:layout] == 'true' ? true : :layout_more
phlex MoreDetailsView.new, layout: layout
end

get '/more-with-haml-layout' do
phlex MoreDetailsView.new, layout: true, layout_engine: :haml
end

get '/stream-with-layout' do
phlex FooView.new, layout: true, stream: true
end

get '/svg' do
phlex SvgElem.new
end

get '/svg-with-layout' do
phlex SvgElem.new, layout: true
end

get '/svg/plain' do
phlex SvgElem.new, content_type: :text
end
Expand All @@ -92,21 +90,6 @@ class TestApp < Sinatra::Application
end
end

# Trick Capybara into managing Puma for us.
class NeedsServerDriver < Capybara::Driver::Base
def needs_server?
true
end
end

Capybara.register_driver :needs_server do
NeedsServerDriver.new
end

Capybara.app = TestApp
Capybara.default_driver = :needs_server
Capybara.server = :puma, { Silent: true }

RSpec.describe Phlex::Sinatra do
include Rack::Test::Methods

Expand Down Expand Up @@ -157,15 +140,48 @@ def app
end
end

context 'with a Phlex::SVG view' do
it 'responds with the correct content type by default' do
context 'when a layout is passed' do
it 'uses the specified layout' do
get '/more-with-layout'

expect(last_response.body).to start_with('<div><pre>')
end

it "uses Sinatra's default layout when `true`" do
get '/more-with-layout', { layout: 'true' }

expect(last_response.body).to start_with('<main><pre>')
end

it 'raises an error stream=true' do
expect {
get('/stream-with-layout')
}.to raise_error(Phlex::Sinatra::IncompatibleOptionError)
end

it 'works with non-ERB templates' do
get '/more-with-haml-layout'

expect(last_response.body).to start_with("<article>\n<pre>")
end
end

context 'when passed a Phlex::SVG view' do
it 'defaults to SVG content type' do
get '/svg'

expect(last_response.body).to start_with('<svg><rect')
expect(last_response.media_type).to eql('image/svg+xml')
end

it 'can also specify a content type' do
it 'does not default to SVG content type if a layout is specified' do
get '/svg-with-layout'

expect(last_response.body).to start_with('<main><svg><rect')
expect(last_response.media_type).to eql('text/html')
end

it 'can also have its content type specified' do
get '/svg/plain'

expect(last_response.body).to start_with('<svg><rect')
Expand Down Expand Up @@ -201,24 +217,4 @@ def app
expect(last_response.media_type).to eql('text/html')
end
end

context 'when streaming' do
def get2(path)
Net::HTTP.start(
Capybara.current_session.server.host,
Capybara.current_session.server.port,
) { |http|
http.get(path)
}
end

it 'outputs the full response' do
last_response = get2('/stream')

expect(last_response.body).to eql('<html><head><title>Streaming</title></head><body><p>1</p><p>2</p></body></html>')

# Indicates that Sinatra's streaming is being used.
expect(last_response['Content-Length']).to be_nil
end
end
end
1 change: 0 additions & 1 deletion spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# frozen_string_literal: true

require 'capybara/rspec'
require 'phlex-sinatra'
require 'rack/test'
require 'sinatra/base'
Expand Down
63 changes: 63 additions & 0 deletions spec/streaming_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
require 'capybara/rspec'

class StreamingView < Phlex::HTML
def view_template
html {
head {
title { 'Streaming' }
}
body {
p { 1 }
flush # Internal private Phlex method.
p { 2 }
}
}
end
end

class StreamingApp < Sinatra::Application
set :environment, :test

get '/stream' do
phlex StreamingView.new, stream: true
end
end

# Trick Capybara into managing Puma for us.
class NeedsServerDriver < Capybara::Driver::Base
def needs_server?
true
end
end

Capybara.register_driver :needs_server do
NeedsServerDriver.new
end

Capybara.app = StreamingApp
Capybara.default_driver = :needs_server
Capybara.server = :puma, { Silent: true }

RSpec.describe Phlex::Sinatra do
attr_reader :last_response

def get(path)
Net::HTTP.start(
Capybara.current_session.server.host,
Capybara.current_session.server.port,
) { |http|
@last_response = http.get(path)
}
end

context 'when streaming' do
it 'outputs the full response' do
get('/stream')

expect(last_response.body).to eql('<html><head><title>Streaming</title></head><body><p>1</p><p>2</p></body></html>')

# Indicates that Sinatra's streaming is being used.
expect(last_response['Content-Length']).to be_nil
end
end
end
1 change: 1 addition & 0 deletions spec/views/layout.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<main><%= yield %></main>
2 changes: 2 additions & 0 deletions spec/views/layout.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
%article
!= yield
1 change: 1 addition & 0 deletions spec/views/layout_more.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div><%= yield %></div>

0 comments on commit c81f764

Please sign in to comment.