Skip to content

1.10.0

Compare
Choose a tag to compare
@joeldrapper joeldrapper released this 05 Apr 15:40
· 160 commits to main since this release

Highlights

[Experimental] Selective Rendering

When rendering Phlex::HTML or Phlex::SVG components, you can now pass a list of ids to be rendered and Phlex will skip past everything else.

This is incredibly useful for Turbo/HTMX-style “HTML-Over-The-Wire” responses. It allows you to render just the parts you need so you don’t need to set up special endpoints for those fragments.

This feature will become more useful in 2.0, which will ship with an optional frontend package, Phlex.js, an alternative to Turbo as well as Morphlex, our DOM morphing library, an alternative to Idiomorph.

To use selective rendering, pass an array of IDs as the fragments: keyword argument when calling call.

[Experimental] Component Kits (limited to Ruby 3.2 and up)

Kits allow you to render components in your templates without using the render method or calling .new.

For this to work, the components need to be in a “Kit”. A Kit is a special Ruby module that contains components. In your app, you might just call this module Components. But you could call it anything and UI kits can ship gems with Phlex Kits in them. The module should extend Phlex::Kit like this:

module Components
  extend Phlex::Kit
end

Now you can define a component in that Kit like this:

class Components::Card < Phlex::HTML
  def initialize(title)
    @title = title
  end
  
  def view_template(&)
    article(class: "card") {
      h1(class: "card-title") { @title }
      yield
    }
  end
end

When this component class is defined, a method will automatically be defined on the containing kit (Components) with the same name as the component, in this case, the method Card will be defined on Components.

Now, in another component, you can use that method to render the card. Note, we don’t need to use render or .new.

def view_template
  Components::Card("Hello") { "Hello world!" }
end

You might not have seen Ruby like this before. It looks like a constant lookup, but it’s actually a method call to the method named Card on the Components module.

It’s disambiguated by parentheses or brackets. You can use empty parentheses if the component takes no arguments, e.g. Components::Navigation() since Components::Navigation without parentheses would be a constant lookup rather than a method call.

Going further, you can actually include the Kit into a component to access these methods without the namespace.

class Posts::IndexView < ApplicationView
  include Components
  
  def view_template
    Card("Hello") { "Hello world!" }
  end
end

All components in a Kit automatically include their own Kit, so they can reference other components in the same Kit. For example, if we defined Components::Table, we would be able to render that as just Table() from Components::Card.

Kits are also compatible with Ruby autoloading and Zeitwerk. If you call the method for a component before the component constant itself is called, it will be auto-loaded anyway.

Using capital-letter method names give us a few advantages:

  1. There is a direct mapping between the constant name of the component class and the name of the method you need to call. No need for any mental (or computational) inflection.
  2. Language Servers such as RubyLSP and Solargraph, which provide auto-complete for constants will essentially auto-complete the methods for rendering those components, without the parentheses.
  3. There’s no chance of a conflict with an HTML element. The Article method renders the Article component, while the article method renders the <article> tag.
  4. Components stand out from other method calls so it’s clear where the boundaries are.
  5. Finally, there’s some existing convention for using capital-letter names for components from JavaScript frameworks such as Svelte and React.

This feature might seem a bit weird at first, but it is 100% pure Ruby, and I think if you give it a minute, you’ll really love it. If Rubocop complains, just disable those rules. ACAB.

If you don’t like it, you can continue using render Foo.new instead.

CSV Views

In addition to Phlex::HTML and Phlex::SVG, you can now render CSV views with Phlex::CSV.

Define a CSV view by subclassing Phlex::CSV and defining a view_template that takes a single item from your collection. Within that template, you can define columns with the column method. The first argument is the column heading, the second is the value:

class ProductsCsv < Phlex::CSV
  def view_template(product)
    column "Name", product.name
    column "Price", product.price
  end
end

Now you can create a ProductsCsv object with an enumerable list of products and call it to render a CSV.

ProductsCsv.new(@products).call

This will actually raise an error because you need to decide whether or not to escape CSV formula injection attacks.

Formula injection is when malicious user data can trigger dangerous formula when the CSV file is opened in Microsoft Excel, Google Spreadsheets, etc. Phlex can escape formula injection, but doing so can compromise the data integrity, since it has to add escape sequences in some cases. See https://owasp.org/www-community/attacks/CSV_Injection

It’s your call whether the CSV is for export/import (where data integrity is key), or for viewing in a spreadsheet editor (where protection from formula injection may be necessary). That’s why you must define escape_csv_injection? to return true or false. Unfortunately, there is no good default for this and that’s the reality of CSVs since Microsoft, Google, etc. won’t make their apps more secure when importing CSVs with formula.

Our updated CSV component looks like this:

class ProductsCsv < Phlex::CSV
  def escape_csv_injection? = true
  
  def view_template(product)
    column "Name", product.name
    column "Price", product.price
  end
end

Major Performance Improvements

Phlex used to slow down as its buffer grew. The performance profile appears to have been approximately O(n), but a new change makes this O(1). Our benchmark rendered seven times as fast after this change. Your mileage may vary — the larger the view, the better the improvements — but every view should be faster overall.

Better Streaming Tools

These have been experimental for a little while, but they’re now generally available:

  • flush will flush the contents of the internal buffer to the output buffer, which is useful if your output buffer is a streaming enumerator.
  • await(task) will wait on an asynchronous task. If the task has already completed, it will just return the result, otherwise it will trigger a flush before waiting for the result. Tasks from the async and concurent-ruby libraries are supported.

No Runtime Dependencies

We removed every last one. 🎉

Deprecations

The big one here is we’re changing the way you define templates. Going forward, instead of defining the template method, you should instead define the view_template method.

Using template will continue to work with a warning message for now, but in 2.0, this method will be used to render the <template> tag instead.

The other deprecation is the <param> element, which will also be removed from 2.0.

Other notable changes

  • Phlex will now try to call to_s on attribute values that don’t respond to to_str
  • mix does a better job when mixing different types of attributes
  • You can now use 💪 instead of the Phlex constant, e.g. 💪::HTML

What's next?

We will soon release an update to Phlex Rails to take advantage of some of these new features. After that, we'll get to work on Phlex 2.0, which will hopefully promote all these experimental features.

If you can help with documentation, graphic design, implementing or testing features, performance profiling, figuring out test coverage, integrations with external libraries, or architecting Ruby or TypeScript/JavaScript software, come and join us on Discord.

If you’re interested in contributing, but don't know where to start, let me know and maybe we can pair on something.

We’re trying to build the best front-end story for Ruby. And we've got big plans.

Thanks to everyone who contributed to this release. And thanks to all my sponsors! ❤️

— Joel

PRs merged

New Contributors

Full Changelog: 1.9.1...1.10.0