ruby

Rendering Samples with Showcase for Ruby on Rails

Alexandre Ruban

Alexandre Ruban on

Rendering Samples with Showcase for Ruby on Rails

In parts one and two of this series, we familiarized ourselves with the ins and outs of Showcase.

Now, we'll dive into samples, Showcase's main feature. Samples show how a component can be used in a real application.

Samples in our Ruby App

In our case, we have two samples, one for a small button and one for a large button:

Showcase button samples

Let's look at how the samples are rendered. We'll add one sample back to our preview file:

erb
<%# test/dummy/app/views/showcase/previews/components/_button.html.erb %> <% showcase.badge :partial, :component %> <% showcase.description "Button is our standard element for what to click on" %> <% showcase.sample "Large", description: "This is our larger button" do %> <%= render "components/button", content: "Button content", mode: :large %> <% end %>

If we refresh the page in our browser, it should look like this:

Showcase large button sample

Sample Methods in Showcase for Ruby on Rails

From our understanding of the gem's architecture, the showcase.sample method populates the @samples instance variable of the Showcase::Preview instance, which is then passed to the final view. Let's take a look at the Showcase::Preview#sample method:

Ruby
# app/models/showcase/preview.rb class Showcase::Preview def sample(name, **options, &block) @samples << Showcase::Sample.new(@view_context, name, **options).tap { _1.evaluate(&block) } end end # => @samples = [Showcase::Sample instance]

And the Showcase::Sample initializer:

Ruby
# app/models/showcase/sample.rb class Showcase::Sample attr_reader :name, :id, :events, :details attr_reader :rendered, :source, :instrumented def initialize(view_context, name, description: nil, id: name.parameterize, syntax: :erb, events: nil, **details) @view_context = view_context @name, @id, @syntax, @details = name, id, syntax, details @events = Array(events) description description if description end def description(content = nil, &block) @description = content || @view_context.capture(&block) if content || block_given? @description end end

There is nothing very surprising in this initializer; most of it is just setting instance variables. Here is the current state of our Showcase::Sample instance:

Ruby
Showcase::Sample.new(view_context, "Large", description: "This is our larger button") # => @name = "Large" # => @id = "large" # => @description = "This is our larger button" # => @syntax = :erb # => @details = {} # => @events = [] # => @view_context = The view context helper

Now that we have our instance, we can call the evaluate method on it:

Ruby
# app/models/showcase/sample.rb class Showcase::Sample def evaluate(&block) if block.arity.zero? consume(&block) else @view_context.capture(self, &block) end end end

About block.arity

If you are not familiar with block.arity, it returns the number of arguments that the block expects. If the block does not expect any arguments, block.arity will return 0. In our case, the block is the following:

erb
<% showcase.sample "Large", description: "This is our larger button" do %> <%= render "components/button", content: "Button content", mode: :large %> <% end %>

As we can see, our block does not expect any arguments, so block.arity will return 0. This means that the consume method will be called, with the block as an argument:

Ruby
# app/models/showcase/sample.rb class Showcase::Sample def consume(&block) render(&block) extract_source(&block) end end

About the consume Method

The consume method calls the render and extract_source methods, passing our block as an argument. Let's have a look at the render method first:

Ruby
# app/models/showcase/sample.rb class Showcase::Sample def render(&block) # TODO: Remove `is_a?` check when Rails 6.1 support is dropped. assigns = proc { @instrumented = _1 if _1.is_a?(ActiveSupport::Notifications::Event) } ActiveSupport::Notifications.subscribed(assigns, "render_partial.action_view") do @rendered = @view_context.capture(&block) end end end

The render method uses ActiveSupport::Notifications to subscribe to the render_partial.action_view event. This event is triggered whenever a partial is rendered in Rails. In our case, the block passed to the showcase.sample method will render the components/button partial, so an event will be triggered and the @instrumented instance variable will be set to the ActiveSupport::Notifications::Event published by the rendering of the partial.

This ActiveSupport::Notifications::Event instance responds to the duration and allocation methods. It will be used later to display the duration and allocations of the sample in the view, as shown in the top right corner here:

Showcase button sample with duration and allocations

Calling extract_source

Next, the extract_source method is called, with the block as an argument:

Ruby
# app/models/showcase/sample.rb class Showcase::Sample def extract_source(&block) source = extract_source_block_via_matched_indentation_from(*block.source_location) @source = @view_context.instance_exec(source, @syntax, &Showcase.sample_renderer) end end

The extract_source method first calls extract_source_block_via_matched_indentation_from with *block.source_location as arguments. If you're not familiar with block.source_location, it returns an array of two elements: the file path and line number where the block was defined. In our case, it will return something like this:

Ruby
[ "/path/to/test/dummy/app/views/showcase/previews/components/_button.html.erb", 4 # The line number where the block was defined ]

The splat operator * before block.source_location is used to unpack the array into two separate arguments: the file path and the line number.

A More Complex Method

Let's have a look at the extract_source_block_via_matched_indentation_from method:

Ruby
# app/models/showcase/sample.rb class Showcase::Sample def extract_source_block_via_matched_indentation_from(file, source_location_index) # `Array`s are zero-indexed, but `source_location` indexes are not, hence `pred`. starting_line, *lines = File.readlines(file).slice(source_location_index.pred..) indentation = starting_line.match(/^\s+/).to_s matcher = /^#{indentation}\S/ index = lines.index { _1.match?(matcher) } lines.slice!(index..) if index lines.join.strip_heredoc end end

This is quite a complex method to read, so let's analyze it step by step. File.readlines(file) reads all the lines of the file and returns them as an array. In our case, the result looks like this:

Ruby
File.readlines(file) # [ # "<% showcase.badge :partial, :component %>\n", # "<% showcase.description \"Button is our standard element for what to click on\" %>\n", # "\n", # "<% showcase.sample \"Large\", description: \"This is our larger button\" do %>\n", # " <%= render \"components/button\", content: \"Button content\", mode: :large %>\n", # "<% end %>\n" # ]

The slice(source_location_index.pred..) method call gets lines starting from where the block was defined. The pred method is used to get the previous index, as source_location returns a 1-based index, while Ruby arrays are 0-based:

Ruby
File.readlines(file).slice(source_location_index.pred..) # [ # "<% showcase.sample \"Large\", description: \"This is our larger button\" do %>\n", # " <%= render \"components/button\", content: \"Button content\", mode: :large %>\n", # "<% end %>\n" # ]

If we now view the whole line, the starting_line variable contains the line where the block was defined, and the lines variable contains all the lines after it:

Ruby
starting_line, *lines = File.readlines(file).slice(source_location_index.pred..) starting_line # "<% showcase.sample \"Large\", description: \"This is our larger button\" do %>\n" lines # [ # " <%= render \"components/button\", content: \"Button content\", mode: :large %>\n", # "<% end %>\n" # ] # ...

Then, the indentation variable will store the leading whitespace of the starting_line. The regex /^\s+/ matches one or more whitespace characters at the beginning of a string, and to_s converts the match result to a string. In our case, the starting_line doesn't have any leading whitespace, so indentation is an empty string.

Ruby
indentation = starting_line.match(/^\s+/).to_s # ""

Then the matcher regex will match lines that start with the same indentation, followed by a non-whitespace character. If our code is properly indented, the index variable will return the index of the block's end keyword:

Ruby
matcher = /^#{indentation}\S/ index = lines.index { _1.match?(matcher) } # 1

Finally, lines.slice!(index..) removes all the lines from the index to the end of the array. In our case, it will remove the last line of the block, the end keyword:

Ruby
lines.slice!(index..) if index # [ # " <%= render \"components/button\", content: \"Button content\", mode: :large %>\n" # ]

The last step is simply to convert the array of lines back to a string and trim extra white space:

Ruby
lines.join.strip_heredoc # "<%= render \"components/button\", content: \"Button content\", mode: :large %>\n"

Phew! That was quite a complicated method, but we managed to get through it by reading carefully, line by line.

Back To extract_source

We can now go back to our extract_source method and try to understand the last line of code:

Ruby
# app/models/showcase/sample.rb class Showcase::Sample def extract_source(&block) source = extract_source_block_via_matched_indentation_from(*block.source_location) @source = @view_context.instance_exec(source, @syntax, &Showcase.sample_renderer) end end # source = "<%= render \"components/button\", content: \"Button content\", mode: :large %>\n" # @syntax = :erb

Understanding this line requires a solid grasp of the Ruby language. First, we have to know what Showcase.sample_renderer is:

Ruby
# lib/showcase.rb module Showcase def self.sample_renderer @sample_renderer ||= begin gem "rouge" require "rouge" formatter = Rouge::Formatters::HTML.new @sample_renderer = ->(source, syntax) do lexed = Rouge::Lexer.find(syntax).lex(source) formatter.format(lexed).html_safe end rescue LoadError proc { _1 } end end end

The Rouge Gem in Showcase for Ruby on Rails

By default, Showcase.sample_renderer uses the rouge gem to highlight the syntax of the source code. However, developers using the showcase engine could choose a different syntax highlighter, as the sample_renderer is configurable:

Ruby
# lib/showcase.rb module Showcase # This line means that the `sample_renderer` can be overridden singleton_class.attr_writer :sample_renderer end

If we remember correctly, the rouge gem is not listed in the showcase.gemspec file, which means that it is required to use the showcase engine:

Ruby
# lib/showcase.rb module Showcase def self.sample_renderer @sample_renderer ||= begin gem "rouge" require "rouge" # ... rescue LoadError proc { _1 } end end end

The rescue LoadError idiom means that if rouge is not present in the Gemfile, the sample_renderer will return a simple proc that returns the source code without any syntax highlighting. If it succeeds, it will return a lambda function that takes two arguments: source and syntax, and a highlighted version of the source code.

We can actually test this in the Rails console:

Ruby
source = "<%= render \"components/button\", content: \"Button content\", mode: :large %>\n" syntax = :erb Showcase.sample_renderer.call(source, syntax) # "<span class=\"cp\">&lt;%=</span> <span class=\"n\">render</span> <span class=\"s2\">\"components/button\"</span><span class=\"p\">,</span> <span class=\"ss\">content: </span><span class=\"s2\">\"Button content\"</span><span class=\"p\">,</span> <span class=\"ss\">mode: :large</span> <span class=\"cp\">%&gt;</span>\n"

The funny cp, n, s2, p, and ss classes are the CSS classes used by Rouge to style the syntax highlighting, as shown in the following picture:

Showcase syntax highlighting

Now that we have a good understanding of the Showcase.sample_renderer, we can go back to the extract_source method and ask ourselves why the last line of code is so complicated:

Ruby
# app/models/showcase/sample.rb class Showcase::Sample def extract_source(&block) source = extract_source_block_via_matched_indentation_from(*block.source_location) @source = @view_context.instance_exec(source, @syntax, &Showcase.sample_renderer) end end

In fact, if we make a small experiment and change it to use the Showcase.sample_renderer directly, it will work just as well:

Ruby
# app/models/showcase/sample.rb class Showcase::Sample def extract_source(&block) source = extract_source_block_via_matched_indentation_from(*block.source_location) @source = Showcase.sample_renderer.call(source, @syntax) end end

So why does the original code use instance_exec?

As we are in a gem, we need to allow users of the gem to use it in a wide variety of contexts. The instance_exec method allows us to execute the Showcase.sample_renderer lambda in the context of the @view_context. This makes all view helpers available in the lambda, which could be useful if the sample_renderer needed to use any view helper methods, such as sanitize, link_to, or any other method that is available in the view context. However, this is a very niche use case and probably not useful in 99% of situations.

Sample Instance Variables Render the View

We are finally done with our Showcase::Sample instance. Its instance variables now hold all the data necessary to render our view:

Ruby
# => @name = "Large" # => @id = "large" # => @description = "This is our larger button" # => @source = "<span class=\"cp\">&lt;%=</span>...<span class=\"cp\">%&gt;</span>\n" # => @rendered = "<%= render \"components/button\", content: \"Button content\", mode: :large %>\n" # => @instrumented = ActiveSupport::Notifications::Event instance

In the view, we can render the "showcase/engine/sample" partial for each of the preview's samples:

erb
<%# app/views/showcase/engine/_preview.html.erb %> <% if preview.samples.any? %> <section> <h2 class="...">Samples</h2> <%= render partial: "showcase/engine/sample", collection: preview.samples %> </section> <% end %>

If we have a look at the "showcase/engine/sample" partial, it renders a link with the name of the sample:

erb
<%# app/views/showcase/engine/_sample.html.erb %> <%= link_to sample.name, "##{sample.id}", class: "..." %>

It also renders figures to show the time needed to render the component and its number of allocations:

erb
<%# app/views/showcase/engine/_sample.html.erb %> <% if event = sample.instrumented %> <div class="..."> <span><%= event.duration.round(1) %>ms</span> <span><%= event.allocations %> allocs</span> </div> <% end %>

It renders the actual HTML of the component:

erb
<%# app/views/showcase/engine/_sample.html.erb %> <% if sample.rendered %> <section class="..."> <%= sample.rendered %> </section> <% end %>

And finally, it renders the source code necessary to render the component:

erb
<%# app/views/showcase/engine/_sample.html.erb %> <% if sample.source %> <details> <summary class="...">View Source</summary> <section class="..."> <pre><%= sample.source %></pre> </section> </details> <% end %>

We now understand how all values are computed. There is no more magic left for us to uncover in the Showcase gem!

Wrapping Up

In this series on Showcase, we learned how Rails engines work: their main files and how to run them locally. But we also learned much more than that. We broadened our knowledge of Ruby and Rails, learning about block.arity, the view_context helper, ActiveSupport::Notifications, and how to use rouge to highlight code syntax in our Rails applications.

Instead of relying solely on documentation, we can gain a much deeper understanding of Ruby by reading source code. We can even end up adding new features to engines and contributing to open-source, improving our skills and giving back to the community.

Happy coding!

Wondering what you can do next?

Finished this article? Here are a few more things you can do:

  • Share this article on social media
Alexandre Ruban

Alexandre Ruban

Our guest author Alexandre is a freelance Ruby on Rails developer and author of the Turbo Rails tutorial. He enjoys writing about Ruby on Rails and reading open source code. He has contributed to Ruby on Rails, Hotwire, and Phlex.

All articles by Alexandre Ruban

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps