Skip to content

1.2.0

Choose a tag to compare

@joeldrapper joeldrapper released this 24 Jan 10:18
· 586 commits to main since this release

Highlights

Initialize a view instance with a block

You can now optionally pass a content block to .new instead of passing it to #render. You won't need to handle it in your initializer as it will continue to be passed to the template method. This means you no longer need to worry about the precedence of brace blocks vs do blocks but also means you can instantiate a view with its content and save it somewhere to be rendered later. It also means you can use argument forwarding to forward a method’s arguments to the initialiser of a different view.

For example, this Nav::Link view takes the arguments text and to::

class Nav::Link < Phlex::HTML
  def initialize(text, to:)
    @text, @to = text, to
  end

  def template
    li { a(href: @text) { @text } }
  end
end

We can define a public method on the Nav view that forwards its arguments to the Nav::Link initializer.

class Nav < Phlex::HTML
  def template(&)
    nav { ul(&) }
  end

  def link(...)
    render Link.new(...)
  end
end

Then we can use the Nav component like this.

render Nav do |n|
  n.link "Home", to: "/"
  n.link "About", to: "/about"
  n.link "Contact", to: "/contact"
end

[Experimental] DeferredRender pattern

The above technique won't help if you need to render slots more than once or out-of-order. In this case, we need to yield the block early and use public methods to capture all the slots before rendering the template. DeferredRender does just that. Here's how you could use it to build a tabs component where the tab labels are output before the tab contents.

class Tabs < Phlex::HTML
  # Create a data structure for each of our tabs
  Tab = Data.define(:name, :content)
  
  # Include DeferredRender for an early yield
  include DeferredRender
  
  def initialize
    # Create an empty list of tabs
    @tabs = []
  end
  
  # The template is rendered after yielding the block, so there's no content block to render.
  # Instead, the idea is to use the captured `@tabs` list, which we do in `#navigation` and `#contents`.
  def template
    div(class: "tabs") do
      navigation
      contents
    end
  end
  
  # This is called while yielding the block, so the caller can add tabs to our list
  def add_tab(name, &content)
    @tabs << Tab.new(name:, content:)
  end
  
  private
  
  # Both `navigation` and `contents` iterate through the list of tabs that we collected during the yield.
  def navigation
    ul(class: "tab-navigation") do
      @tabs.each do |tab|
        li { tab.name }
      end
    end
  end
  
  def contents
    div(class: "tab-contents") do
      @tabs.each do |tab|
        article(&tab.content)
      end
    end
  end
end

Underscored tag methods

Sometimes we need to override a tag method. If you’ve done this and need to get back to the original, you can now use _tagname.

Unbuffered decorator

You’ll probably never need to use this, but phlex-rails will use it for compatibility with ERB. The Unbuffered decorator can wrap a yielded view so that methods called on it return the captured result rather than immediately appending something to the output buffer. This means builder views (like the Nav example above) can behave like normal builders in ERB. We’ll share more details about this change in the next release of phlex-rails

Float now goes through object formatters

It's reasonable to want to customise the precision when outputting a float, so you can do the with format_object now.

def format_object(object)
  case object
  when Float
    object.round(2)
  else
    super
  end
end

Removed experimental Turbo views

@marcoroth’s new gem turbo-ruby uses Phlex to output Turbo elements, so we’ll probably switch to that in phlex-rails.

What's Changed

New Contributors

Full Changelog: 1.1.0...1.2.0