1.10.0
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:
- 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.
- 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.
- There’s no chance of a conflict with an HTML element. The
Article
method renders theArticle
component, while thearticle
method renders the<article>
tag. - Components stand out from other method calls so it’s clear where the boundaries are.
- 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 theasync
andconcurent-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 toto_str
mix
does a better job when mixing different types of attributes- You can now use
💪
instead of thePhlex
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
- phlex.gemspec: Drop exe/ mentions by @olleolleolle in #604
- phlex.gemspec: Drop generated comments [ci skip] by @olleolleolle in #605
- CI: tell Dependabot to update GH Actions by @olleolleolle in #603
- Bump actions/checkout from 3 to 4 by @dependabot in #606
- Apply consistent symbol/string attribute transformation for Hash values by @willcosgrove in #610
- Use
yield_content
inwhitespace
by @willcosgrove in #611 - Move the Helpers include to parent class so Phlex::SVG subclasses have access by @davekaro in #621
- Remove concurrent-ruby dependency by @joeldrapper in #634
- Remove ERB dependency by @joeldrapper in #635
- Remove Zeitwerk by @joeldrapper in #633
- Update HTML elements by @joeldrapper in #631
- Rename
template
→view_template
by @joeldrapper in #630 - Attribute values > to_phlex_attribute_value, to_str, to_s by @joelmoss in #629
- Add support for using the
mix
helper with different types by @willcosgrove in #620 - Require
set
by @joeldrapper in #641 - Simplify calls to
register_element
by @brandondrew in #639 - Simplify Element Registration (cont.) by @willcosgrove in #642
- 💪 by @joeldrapper in #654
- Phlex::CSV by @joeldrapper in #655
- Warn when using
<param>
element by @joeldrapper in #658 - Prevent CSV injection attacks by @joeldrapper in #662
- Rename
target
→buffer
by @joeldrapper in #666 - Selective rendering by @joeldrapper in #667
- Non-breaking Public Context by @willcosgrove in #668
- Yield self if block given by @joeldrapper in #669
- Introduce Phlex::Bucket by @joeldrapper in #674
- Support selecting multiple fragments by @joeldrapper in #670
- Halt rendering early once all fragments have been found by @willcosgrove in #671
- Render from namespaced component buckets by @joeldrapper in #676
- Eager load bucket constants by @joeldrapper in #677
- Lazy load autoloaded bucket components by @joeldrapper in #678
- Rename
Bucket
→Kit
by @joeldrapper in #680 - Remove
Phlex::ConcurrentMap
by @joeldrapper in #681 - Change
file_name
tofilename
by @joeldrapper in #684 - Update CI matrix by @joeldrapper in #685
- Improve performance by using bytesize when comparing if the string changed by @davekaro in #690
- Fix for components that render content before a fragment is found by @davekaro in #691
- Update BlackHole to use
bytesize
by @joeldrapper in #692 - Test await and flush by @joeldrapper in #697
- Test custom object method call precedence by @sullyvannunes in #700
- Selective capture blocks by @joeldrapper in #702
New Contributors
- @olleolleolle made their first contribution in #604
- @brandondrew made their first contribution in #639
- @sullyvannunes made their first contribution in #700
Full Changelog: 1.9.1...1.10.0