Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/camertron/rux-rails

Rux view components on Rails.
https://github.com/camertron/rux-rails

Last synced: about 2 months ago
JSON representation

Rux view components on Rails.

Awesome Lists containing this project

README

        

## rux-rails

![Unit Tests](https://github.com/camertron/rux-rails/actions/workflows/unit_tests.yml/badge.svg?branch=master)

Easily write [rux](https://github.com/camertron/rux) view components in Rails.

## View Components

If you haven't already, head over to [viewcomponent.org](https://viewcomponent.org/) or watch Joel Hawksley's excellent 2019 [Railsconf talk](https://www.youtube.com/watch?v=y5Z5a6QdA-M) on writing view components before reading the rest of this README.

## What's Rux?

View components are awesome, but they're a little cumbersome to work with. The view itself is a separate file, and you have to write a bunch of `render` statements all over the place. What if you could write HTML directly inside your view components like some fancy Javascript dev?

Well now you can! [Rux](https://github.com/camertron/rux) makes it possible to write HTML tags and render components _inside_ your view component classes, and rux-rails brings that goodness to Rails.

## An Example

Let's take a look at one iteration of Joel's issue badge example. The original code looks like this:

```ruby
module Issues
class Badge < ViewComponent::Base
include OcticonsHelper

def initialize(issue:)
@issue = issue
end

def template
<<~erb
<% if @issue.closed? %>
<%= render Primer::State, color: :red, title: "Status: Closed" do %>
<%= octicon('issue-closed') %> Closed
<% end %>
<% else %>
<%= render Primer::State, color: :green, title: "Status: Open" do %>
<%= octicon('issue-opened') Open %>
<% end %>
<% end %>
erb
end
end
end
```

Here's the equivalent written in rux (sorry about the syntax highlighting - Github doesn't know about rux yet):

```ruby
module Issues
class Badge < ViewComponent::Base
include OcticonsHelper

def initialize(issue:)
@issue = issue
end

def call
if @issue.closed?

{octicon('issue-closed')} Closed

else

{octicon('issue-opened')} Open

end
end
end
end
```

In my humble opinion, the rux version:

1. is easier to read without all the ERB syntax.
1. makes the connection between Ruby and HTML more obvious.
1. allows embedding Ruby more naturally with curly braces.
1. makes `render` calls implicit.

## Getting Started

Integrating rux into your Rails app is pretty straightforward.

1. Add rux-rails to your Gemfile, eg:
```ruby
gem 'rux-rails', '~> 1.0'
```
1. Run `bundle install`
1. ... that's it.

Now any file with a .rux extension your Rails app loads (i.e. `require`s) will get transparently transpiled and loaded.

### Development Environment

By default, rux-rails will automatically transpile .rux files on load in the development and test environments only. In addition, any changes you make to your .rux files will get picked up without having to restart your Rails server, much the same way changes to controllers, etc are handled. You can manually enable or disable automatic transpilation per-environment.

For example, to disable it in development, add the following line to your config/environments/development.rb file:

```ruby
config.rux.transpile = false
```

### Production Environment

By default, `config.rux.transpile` is set to `false` in the production environment, which means rux-rails' monkeypatches to `Kernel`, etc aren't loaded in production (which is a good thing). You could choose to turn it on, but automatic transpilation of .rux files is disabled automatically in the production environment for the same reasons the asset pipeline is disabled. Your view components (and static assets) don't change once deployed, so it's more efficient to precompile (or pre-transpile) them before deploying. Rux-rails comes with a handy rake task that can pre-transpile all your .rux templates:

```bash
bundle exec rake rux:transpile
```

I recommend running this rake task at the same time you run `assets:precompile` and/or `webpacker:compile`. The `rux:transpile` task will produce one .rb file for every .rux file it encounters in your app.

## Writing Rux Components

Rux components are just view components that contain rux tags. As with HTML, rux tags can be nested inside one another and can contain Ruby control structures, etc.

Components usually live in the app/components directory. Just as is possible with models, controllers, etc, it's possible to organize your view components into subdirectories as your application design warrants.

Let's take a look at two simple components. The first renders a first and last name, while the second uses the first to compose a greeting:

```ruby
# app/components/name_component.rux
class NameComponent < ViewComponent::Base
def initialize(first_name:, last_name:)
@first_name = first_name
@last_name = last_name
end

def call
{@first_name} {@last_name}
end
end

# app/components/greeting_component.rux
class GreetingComponent < ViewComponent::Base
def call


Hey there !

end
end
```

Then, in one of your Rails views, render `GreetingComponent` like so:

```html+erb
<%# app/views/home/index.html.erb %>
<%= render(GreetingComponent.new) %>
```

When rendered, the view will contain the following HTML:

```html


Hey there Homer Simpson!

```

### Component Contents

View components can also have content bodies, which can be other view components. Use the `content` method where you want the nested content to be rendered. As an example, let's modify our `GreetingComponent`:

```ruby
# app/components/greeting_component.rux
class GreetingComponent < ViewComponent::Base
def call






end
end

# app/components/salutation_component.rux
class SalutationComponent < ViewComponent::Base
SALUTATIONS = ['Hey there', 'Greetings', 'Great to see you'].freeze

def call

{SALUTATIONS.sample} {content}!

end
end
```

When rendered in a view, we get the following HTML:

```html



Greetings Homer Simpson!


```

### Embedding Ruby

As we've already seen, you can embed Ruby code between curly braces. It's important to know however that Ruby code is only allowed for attribute values and content bodies.

Because the wide world of Ruby is available to you, anything goes. For example, let's modify our `GreetingComponent` to say hi to a variable number of people:

```ruby
# app/components/greeting_component.rux
class GreetingComponent < ViewComponent::Base
def initialize(people:)
@people = people
end

def call


{@people.map do |person|

Hey there !

end}

end
end
```

Notice I used `map` to render multiple `NameComponents`. I've got rux in my Ruby in my rux in my Ruby!

Next, let's modify our view to pass in an array of person hashes:

```html+erb
<%# app/views/home/index.html.erb %>
<%= render(
GreetingComponent.new([
{ first_name: 'Homer', last_name: 'Simpson' },
{ first_name: 'Barney', last_name: 'Gumble' },
{ first_name: 'Monty', last_name: 'Burns' }
])
) %>
```

This results in the following HTML:

```html



Greetings Homer Simpson!


Great to see you Barney Gumble!


Hey there Monty Burns!


```

## Rux Templates

In addition to supporting view components, rux also supports rendering rux directly in your Rails views. Just give your view a .ruxt file extension and voila! Rux in your views! As an example, let's rewrite our view from the previous section as a rux template:

```ruby
# app/views/home/index.html.ruxt

```

## How it Works

Rux-rails monkeypatches the `Kernel` module in order to automatically transpile .rux files on `require`. That might be a controversial idea, but it seems to work really well. Here's how it works step-by-step:

1. First, rux-rails attempts to call Ruby's original `require` method.
1. If original `require` raises a `LoadError`, rux-rails searches the Ruby load path for a file with a .rux extension.
1. If a corresponding .rux file exists on disk, rux-rails compiles it loads it using `Kernel.load`.
1. If a corresponding .rux file cannot be found, rux-rails raises the original `LoadError`.

There are also monkeypatches in place for Zeitwerk and `ActiveSupport::Dependencies` to get auto-transpiling working with Rails autoloading (which is absurdly obtuse and complicated). The monkeypatches are necessary mostly because Ruby and Rails assume Ruby files will always have .rb file extensions.

Hit me up if you know of a less invasive way of enabling auto-transpilation.

## Editor Support

Sublime Text: [https://github.com/camertron/rux-SublimeText](https://github.com/camertron/rux-SublimeText)

Atom: [https://github.com/camertron/rux-atom](https://github.com/camertron/rux-atom)

VSCode: [https://github.com/camertron/rux-vscode](https://github.com/camertron/rux-vscode)

## Running Tests

`bundle exec appraisal rake` should do the trick.

## License

Licensed under the MIT license. See LICENSE for details.

## Authors

* Cameron C. Dutro: http://github.com/camertron