A Ruby template language inspired by JSX

Build Status

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:

<body>
  <Hero size="fullscreen" {**splat_some_attributes}>
    <h1>Hello {@name}</h1>
    <p>Welcome to rbexy, marrying the nice parts of React templating with the development efficiency of Rails server-rendered apps.</p>
    <Button to={about_path}>Learn more</Button>
  </Hero>
</body>

after defining them in 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:

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:

require "rbexy/rails/engine"

Not using Rails? See "Usage outside of Rails" below.

Create your first component at app/components/hello_world_component.rb:

class HelloWorldComponent < Rbexy::Component
  def setup(name:)
    @name = name
  end
end

With a template app/components/hello_world_component.rbx:

<div>
  <h1>Hello {@name}</h1>
  {content}
</div>

Add a controller, action, route, and rbx view like app/views/hello_worlds/index.rbx:

<HelloWorld name="Nick">
  <p>Welcome to the world of component-based frontend development in Rails!</p>
</HelloWorld>

Or you can render Rbexy components from ERB with <%= HelloWorldComponent.new(self, name: "Nick").render %>

Fire up rails s, navigate to your route, and you should see Rbexy in action!

Template Syntax

Text

You can put arbitrary strings anywhere.

At the root:

Hello world

Inside tags:

<p>Hello world</p>

As attributes:

<div class="myClass"></div>

Comments

Start a line with # to leave a comment:

# Comments can be at the root
<div>
  # Or within tags
  # spanning multiple lines
  <h1>Hello world</h1>
</div>

Expressions

You can put ruby code anywhere that you would put text, just wrap it in { ... }

At the root:

{"hello world".upcase}

Inside a sentence:

Hello {"world".upcase}

Inside tags:

<p>{"hello world".upcase}</p>

As attributes:

<p class={@dynamic_class}>Hello world</p>

Tags within expressions

To conditionalize your template:

<div>
  {some_boolean && <h1>Welcome</h1>}
  {another_boolean ? <p>Option One</p> : <p>Option Two</p>}
</div>

Loops:

<ul>
  {[1, 2, 3].map { |n| <li>{n}</li> }}
</ul>

As an attribute:

<Hero title={<h1>Hello World</h1>}>
  Content here...
</Hero>

Pass a lambda to a prop, that when called returns a tag:

<Hero title={-> { <h1>Hello World</h1> }}>
  Content here...
</Hero>

Tags

You can put standard HTML tags anywhere.

At the root:

<h1>Hello world</h1>

As children:

<div>
  <h1>Hello world</h1>
</div>

As siblings with other tags:

<div>
  <h1>Hello world</h1>
  <p>Welcome to rbexy</p>
</div>

As siblings with text and expressions:

<h1>Hello world</h1>
{an_expression}
Some arbitrary text

Self-closing tags:

<input type="text" />

Attributes

Text and expressions can be provided as attributes:

<div class="myClass" id={dynamic_id}></div>

Value-less attributes are allowed:

<input type="submit" disabled>

You can splat a hash into attributes:

<div {**{ class: "myClass" }} {**@more_attrs}></div>

Custom components

You can use custom components alongside standard HTML tags:

<div>
  <PageHeader title="Welcome" />
  <PageBody>
    <p>To the world of custom components</p>
  </PageBody>
</div>

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:

# 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:

// app/components/page_header_component.rbx
<h1>{@title}</h1>

You can call this component from another .rbx template file (<PageHeader title="Hello" />)—either one rendered by another component class or a Rails view file like app/views/products/index.rbx. Or you can call it from ERB (or any other template language) like PageHeaderComponent.new(self, title: "Hello").render.

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/.

Template-less components

If you'd prefer to render your components entirely from Ruby, e.g. using Rails tag helpers, you can do so with #call:

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:

<Form>
  <TextField field={:title} />
</Form>

The form component can use Rails form_for and then pass the form builder object down to any field components using context:

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

ViewComponent

Using Github's view_component library? Rbexy ships with a provider that'll resolve your RBX tags like <Button /> to their corresponding ButtonComponent < ViewComponent::Base components.

require "rbexy/component_providers/view_component_provider"

Rbexy.configure do |config|
  config.component_provider = Rbexy::ComponentProviders::ViewComponentProvider.new
end

Other types of components

You just need to tell rbexy how to resolve your custom component classes as it encounters them while evaluating your template by implementing a ComponentProvider:

class MyComponentProvider
  def match?(name)
    # Return true if the given tag name matches one of your custom components
  end

  def render(context, name, **attrs, &block)
    # Instantiate and render your custom component for the given name, using
    # the render context as needed (e.g. ActionView in Rails)
  end
end

# Register your component provider with Rbexy
Rbexy.configure do |config|
  config.component_provider = MyComponentProvider.new
end

Or in Rails you can customize the component provider just for a controller:

class ThingsController < ApplicationController
  def rbexy_component_provider
    MyComponentProvider.new
  end
end

See lib/rbexy/component_providers/ for example implementations.

Usage outside of Rails

Rbexy compiles your template into ruby code, which you can then execute in any context you like, so long as a tag builder is available at #rbexy_tag. We provide a built-in runtime leveraging ActionView's tag helper that you can extend from or build your own:

Subclass to add methods and instance variables that you'd like to make available to your template.

class MyRuntime < Rbexy::Runtime
  def initialize
    super
    @an_ivar = "Ivar value"
  end

  def a_method
    "Method value"
  end
end

Rbexy.evaluate("<p class={a_method}>{@an_ivar}</p>", MyRuntime.new)

If you're using custom components, inject a ComponentProvider (see "Custom components" for an example implementation):

class MyRuntime < Rbexy::Runtime
  def initialize(component_provider)
    super(component_provider)
    @ivar_val = "ivar value"
  end

  def splat_attrs
    {
      key1: "val1",
      key2: "val2"
    }
  end
end

Rbexy.evaluate(
  "<Forms.TextField /><Button prop1=\"val1\" prop2={true && \"val2\">Submit</Button>",
  MyRuntime.new(MyComponentProvider.new)
)

Or implement your own runtime, so long as it conforms to the API:

  • #rbexy_tag that returns a tag builder conforming to the API of ActionView::Helpers::TagHelpers::TagBuilder
  • #evaluate(code) that evals the given string of ruby code

Development

docker-compose build
docker-compose run rbexy rspec

Or auto-run tests with guard if you prefer:

docker-compose run rbexy guard

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/rbexy. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the Rbexy project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.