# A Ruby template language inspired by JSX
[![Build Status](https://travis-ci.org/patbenatar/rbexy.svg?branch=master)](https://travis-ci.org/patbenatar/rbexy)
* [Getting Started](#getting-started-with-rails)
* [Template Syntax](#template-syntax)
* [Components](#components)
* [`Rbexy::Component`](#rbexycomponent)
* [Usage with any component library](#usage-with-any-component-library)
* [Fragment caching in Rails](#fragment-caching-in-rails)
* [Advanced](#advanced)
* [Component resolution](#component-resolution)
* [AST Transforms](#ast-transforms)
* [Usage outside of Rails](#usage-outside-of-rails)
## Manifesto
Love JSX and component-based frontends, but sick of paying the costs of SPA development? Rbexy brings the elegance of JSX—operating on HTML elements and custom components with an interchangeable syntax—to the world of Rails server-rendered apps.
Combine this with CSS Modules in your Webpacker PostCSS pipeline and you'll have a first-class frontend development experience while maintaining the development efficiency of Rails.
_But what about Javascript and client-side behavior?_ You probably don't need as much of it as you think you do. See how far you can get with layering RailsUJS, vanilla JS, Turbolinks, and/or StimulusJS onto your server-rendered components. I think you'll be pleasantly surprised with the modern UX you're able to build while writing and maintaining less code.
## Example
Use your custom Ruby class components from `.rbx` templates just like you would React components in JSX:
```jsx
Hello {@name}
Welcome to rbexy, marrying the nice parts of React templating with the development efficiency of Rails server-rendered apps.
```
after defining them in Ruby:
```ruby
class HeroComponent < Rbexy::Component # or use ViewComponent, or another component lib
def setup(size:)
@size = size
end
end
class ButtonComponent < Rbexy::Component
def setup(to:)
@to = to
end
end
```
with their accompying template files (also can be `.rbx`!), scoped scss files, JS and other assets (not shown).
## Getting Started (with Rails)
Add it to your Gemfile and `bundle install`:
```ruby
gem "rbexy"
```
_From 1.0 onward, we only support Rails 6. If you're using Rails 5, use the 0.x releases._
In `config/application.rb`:
```ruby
require "rbexy/rails/engine"
```
_Not using Rails? See "Usage outside of Rails" below._
Create your first component at `app/components/hello_world_component.rb`:
```ruby
class HelloWorldComponent < Rbexy::Component
def setup(name:)
@name = name
end
end
```
With a template `app/components/hello_world_component.rbx`:
```jsx
Hello {@name}
{content}
```
Add a controller, action, route, and `rbx` view like `app/views/hello_worlds/index.rbx`:
```jsx
Welcome to the world of component-based frontend development in Rails!
```
Fire up `rails s`, navigate to your route, and you should see Rbexy in action!
## Template Syntax
You can use Ruby code within brackets:
```jsx
Hello {"world".upcase}
```
You can splat a hash into attributes:
```jsx
```
You can use HTML or component tags within expressions. e.g. to conditionalize a template:
```jsx
{some_boolean &&
Welcome
}
{another_boolean ?
Option One
:
Option Two
}
```
Or in loops:
```jsx
{[1, 2, 3].map { |n|
{n}
}}
```
Blocks:
```jsx
{link_to "/" do
Click me
end}
```
Pass a tag to a component as an attribute:
```jsx
Hello World}>
Content here...
```
Or pass a lambda as an attribute, that when called returns a tag:
```jsx
{
Hello World
}}>
Content here...
```
_Note that when using tags inside blocks, the block must evaluate to a single root element. Rbexy behaves similar to JSX in this way. E.g.:_
```
# Do
-> { Hello World }
# Don't
-> { Hello World }
```
Start a line with `#` to leave a comment:
```jsx
# Private note to self that won't be rendered in the final HTML
```
## Components
You can use Ruby classes as components alongside standard HTML tags:
```jsx
To the world of custom components
```
By default, Rbexy will resolve `PageHeader` to a Ruby class called `PageHeaderComponent` and render it with the view context, attributes, and its children: `PageHeaderComponent.new(self, title: "Welcome").render_in(self, &block)`. This behavior is customizable, see "Component resolution" below.
### `Rbexy::Component`
We ship with a component superclass that integrates nicely with Rails' ActionView and the controller rendering context. You can use it to easily implement custom components in your Rails app:
```ruby
# app/components/page_header_component.rb
class PageHeaderComponent < Rbexy::Component
def setup(title:)
@title = title
end
end
```
By default, we'll look for a template file in the same directory as the class and with a matching filename:
```jsx
// app/components/page_header_component.rbx
{@title}
```
Your components and their templates run in the same context as traditional Rails views, so you have access to all of the view helpers you're used to as well as any custom helpers you've defined in `app/helpers/` or via `helper_method` in your controller.
#### Template-less components
If you'd prefer to render your components entirely from Ruby, you can do so by implementing `#call`:
```ruby
class PageHeaderComponent < Rbexy::Component
def setup(title:)
@title = title
end
def call
tag.h1 @title
end
end
```
#### Context
`Rbexy::Component` implements a similar notion to React's Context API, allowing you to pass data through the component tree without having to pass props down manually.
Given a template:
```jsx
```
The form component can use Rails `form_for` and then pass the `form` builder object down to any field components using context:
```ruby
class FormComponent < Rbexy::Component
def setup(form_object:)
@form_object = form_object
end
def call
form_for @form_object do |form|
create_context(:form, form)
content
end
end
end
class TextFieldComponent < Rbexy::Component
def setup(field:)
@field = field
@form = use_context(:form)
end
def call
@form.text_field @field
end
end
```
#### Usage with ERB
We recommend using `Rbexy::Component` with the rbx template language, but if you prefer ERB... a component's template can be `.html.erb` and you can render a component from ERB like so:
Rails 6.1:
```erb
<%= render PageHeaderComponent.new(self, title: "Welcome") do %>
Children...
<% end >
```
Rails 6.0 or earlier:
```erb
<%= PageHeaderComponent.new(self, title: "Welcome").render_in(self) %>
```
### Usage with any component library
You can use the rbx template language with other component libraries like Github's view_component. You just need to tell Rbexy how to render the component:
```ruby
# config/initializers/rbexy.rb
Rbexy.configure do |config|
config.component_rendering_templates = {
children: "{capture{%{children}}}",
component: "::%{component_class}.new(%{view_context},%{kwargs}).render_in%{children_block}"
}
end
```
## Fragment caching in Rails
`.rbx` templates integrate with Rails fragment caching, automatically cachebusting when the template or its render dependencies change.
If you're using `Rbexy::Component`, you can further benefit from component cachebusting where the fragment cache will be busted if any dependent component's template _or_ class definition changes.
And you can use ``, a convenient wrapper for the Rails fragment cache:
```rbx
Fragment here...
```
## Advanced
### Component resolution
By default, Rbexy resolves component tags to Ruby classes named `#{tag}Component`, e.g.:
* `` => `PageHeaderComponent`
* `` => `Admin::ButtonComponent`
You can customize this behavior by providing a custom resolver:
```ruby
# config/initializers/rbexy.rb
Rbexy.configure do |config|
config.element_resolver = MyResolver.new
end
```
Where `MyResolver` implements the following API:
* `component?(name: string, template: Rbexy::Template) => Boolean`
* `component_class(name: string, template: Rbexy::Template) => T`
See `lib/rbexy/component_resolver.rb` for an example.
#### Auto-namespacing
Want to namespace your components but sick of typing `Admin.` in front of every component call? Rbexy's default `ComponentResolver` implementation has an option for that:
```ruby
# config/initializers/rbexy.rb
Rbexy.configure do |config|
config.element_resolver.component_namespaces = {
Rails.root.join("app", "views", "admin") => %w[Admin],
Rails.root.join("app", "components", "admin") => %w[Admin]
}
end
```
Now any calls to `